├── generator ├── src │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ └── beans.xml │ │ └── log4j.xml │ │ └── java │ │ └── org │ │ └── ovirt │ │ └── sdk │ │ └── python │ │ ├── Main.java │ │ ├── PythonGenerator.java │ │ ├── PythonClassName.java │ │ ├── PythonReservedWords.java │ │ ├── PythonTypeReference.java │ │ ├── Tool.java │ │ ├── TypesGenerator.java │ │ └── PythonNames.java ├── README.adoc └── pom.xml ├── sdk ├── MANIFEST.in ├── tests │ ├── __init__.py │ ├── pki │ │ ├── README.adoc │ │ ├── server.crt │ │ ├── ca.crt │ │ ├── server.key │ │ ├── ugly.key │ │ ├── localhost.key │ │ ├── ugly.crt │ │ └── localhost.crt │ ├── test_enum.py │ ├── test_xml_writer.py │ ├── test_sso_writer.py │ ├── test_iscsi_discover.py │ ├── test_cluster_reader.py │ ├── test_connection_error.py │ ├── test_check_types.py │ ├── test_affinity_group_reader.py │ ├── test_cluster_service.py │ ├── test_datacenter_service.py │ ├── test_storage_domain_service.py │ ├── test_invalid_authentication.py │ ├── test_fault_reader.py │ ├── test_setupnetworks.py │ ├── test_vm_reader.py │ ├── test_network_reader.py │ ├── test_network_writer.py │ ├── test_connection_create.py │ ├── test_read_link.py │ ├── test_xml_reader.py │ ├── test_writer.py │ ├── server.py │ └── test_vm_service.py ├── ext │ ├── ov_xml_reader.h │ ├── ov_xml_writer.h │ ├── ov_xml_module.h │ ├── ov_xml_utils.h │ ├── xml.c │ ├── ov_xml_module.c │ └── ov_xml_utils.c ├── lib │ └── ovirtsdk4 │ │ ├── http.py │ │ ├── writer.py │ │ ├── reader.py │ │ └── service.py ├── pom.xml └── LICENSE.txt ├── .github ├── CODEOWNERS └── workflows │ ├── build-on-pr.yml │ └── build-on-merge.yml ├── .devcontainer └── devcontainer.json ├── .gitignore ├── Dockerfile ├── README.adoc └── pom.xml /generator/src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sdk/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.adoc 3 | recursive-include ext *.c *.h 4 | recursive-include examples *.py 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/about-code-owners 2 | # Default reviewers for everything 3 | * @oliel @mnecas 4 | -------------------------------------------------------------------------------- /generator/README.adoc: -------------------------------------------------------------------------------- 1 | = oVirt Engine Pyton SDK Generator 2 | 3 | == Introduction 4 | 5 | This project contains the code generator for the oVirt Python SDK. 6 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oVirt-engine-sdk", 3 | "mounts": [ 4 | "source=${localEnv:HOME}/.m2,target=/home/build/.m2,type=bind,consistency=cached" 5 | ], 6 | "build": { 7 | "dockerfile": "../Dockerfile" 8 | }, 9 | "remoteUser": "build", 10 | "updateRemoteUserUID": true 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.egg-info/ 3 | *.gz 4 | *.iml 5 | *.pyc 6 | *.pyo 7 | *.rpm 8 | *.so 9 | *.spec 10 | .bash_history 11 | .eggs/ 12 | .idea/ 13 | exported-artifacts/ 14 | sdk/build/ 15 | sdk/dist/ 16 | sdk/lib/ovirtsdk4 17 | sdk/lib/ovirtsdk4/readers.py 18 | sdk/lib/ovirtsdk4/services.py 19 | sdk/lib/ovirtsdk4/types.py 20 | sdk/lib/ovirtsdk4/writers.py 21 | settings.xml 22 | target/ 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/ovirt/buildcontainer:el9stream 2 | 3 | ARG USERNAME=build 4 | ENV USERNAME=$USERNAME 5 | 6 | RUN dnf install -y \ 7 | git \ 8 | maven-openjdk21 \ 9 | maven-local-openjdk21 \ 10 | gcc \ 11 | libxml2-devel \ 12 | gnupg \ 13 | python3 \ 14 | python3-devel \ 15 | python3-pip \ 16 | && dnf clean all 17 | 18 | RUN python3 -m pip install --upgrade pip \ 19 | && pip3 install flake8 wheel 20 | 21 | # Set default User 22 | USER $USERNAME -------------------------------------------------------------------------------- /sdk/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | -------------------------------------------------------------------------------- /sdk/tests/pki/README.adoc: -------------------------------------------------------------------------------- 1 | = PKI Resources 2 | 3 | This directory contains private key files and certificates used by the 4 | tests: 5 | 6 | ca.crt:: The certificate of the CA that is used to sign all the other 7 | certificates. 8 | 9 | localhost.key:: The private key of the `localhost` certificate. 10 | 11 | localhost.crt:: The certificate for `localhost`. 12 | 13 | ugly.key:: The private key of the `ugly` certificate. 14 | 15 | ugly.crt:: The certificate for `ugly`. 16 | 17 | The `ugly` private key and certificate are intended for tests that 18 | check that connections to servers whose host name doesn't match the 19 | certifcate common name fail. 20 | 21 | All these certificates expire in 100 years, hopefully they will live 22 | longer than the SDK. -------------------------------------------------------------------------------- /sdk/ext/ov_xml_reader.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright oVirt Authors 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 implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __OV_XML_READER_H__ 18 | #define __OV_XML_READER_H__ 19 | 20 | #include 21 | 22 | /* Initialization function: */ 23 | extern void ov_xml_reader_define(void); 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /sdk/ext/ov_xml_writer.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright oVirt Authors 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 implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __OV_XML_WRITER_H__ 18 | #define __OV_XML_WRITER_H__ 19 | 20 | #include 21 | 22 | /* Initialization function: */ 23 | extern void ov_xml_writer_define(void); 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /sdk/tests/test_enum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from ovirtsdk4 import types 20 | 21 | 22 | def test_enum(): 23 | """ 24 | Check Architecture.x86_64 exists and has expected value 25 | """ 26 | assert str(types.Architecture.X86_64) == 'x86_64' 27 | -------------------------------------------------------------------------------- /sdk/ext/ov_xml_module.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright oVirt Authors 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 implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __OV_XML_MODULE_H__ 18 | #define __OV_XML_MODULE_H__ 19 | 20 | #include 21 | 22 | /* The name of the module: */ 23 | #define OV_XML_MODULE_NAME "ovirtsdk4.xml" 24 | 25 | /* Module: */ 26 | extern PyObject* ov_xml_module; 27 | 28 | /* Initialization function: */ 29 | extern void ov_xml_module_define(void); 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /sdk/tests/pki/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICpjCCAY4CCQDhOu9ZcMpGjTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls 3 | b2NhbGhvc3QwIBcNMTYwMzA3MTcyMjMxWhgPMjExMzA4MjYxNzIyMzFaMBQxEjAQ 4 | BgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 5 | AL/bgdVnw5XROOblW4n26Vkm87Pe8HNwRBXnjQOIsQM6iJcLQQRLYi4GDrCKNhGk 6 | KtzsTjf6Uk9WGC7lpJgQNwmkDbzPxdQMtzg+Y7HIlH8mECuYLzbBVyK+FNvch2Rq 7 | hfkN1EscOYWQXPK4eVLvwqZx9u0VkfVp3+6VRSWGrljhEc3HckcSNm8gMT5rYpYu 8 | LATuBh9iNx8+5EC/09ed2owLFean2yOUSKY1YbvLu0cFtsDq5eRhFJTrGzuG8iAd 9 | UnopXHMf78WAtWIsBUDyWgNvXh2cIwmRJJq24GHIpViCm9oOaF7xrlHwZ9+1T/hK 10 | GVXEjCNl4XHVeORv7wFj1p0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAAqYHKRP5 11 | 3gJLSWmczZmc299N/1ECHrxpzmiBEHcq+r+8/vYSPlVReS8c+9hUeQhGJqveqmLu 12 | XvjnN1JOXwKDu2riS88S0X31dsdTX7yFKqIR7AsCT50KfLh6BifTlc4NB4EYQgXA 13 | Y2VpN+elIxhGupaOgx7qeN+hBt1mq3wGnw5VAmygpgX8caHjn+HFlBLSDBfrKxmq 14 | rF8g10PcXq+v7Zlo/FPxbxLLTRoeDjwuOTrMCahXblJOPrDs29Vtiq7Dfs/NxWt/ 15 | UF3S37rh7oSepB/yzJkLwH8zJPz5u1npoBLG9wNBx5SK8a9bqeNAc0MLzUDhGfVM 16 | nK+ILXDLBvkQHg== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /sdk/ext/ov_xml_utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright oVirt Authors 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 implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __OV_XML_UTILS_H__ 18 | #define __OV_XML_UTILS_H__ 19 | 20 | #include 21 | 22 | /* Checks the the given `value` is a string and extracts the content. If the value isn't a string then it generates an 23 | exception indicating the the parameter with name `name` isn't a string, and returns NULL. If the value is a string 24 | returns a newly allocated libxml string. The caller is responsible for freeing it. */ 25 | extern xmlChar* ov_xml_get_string_parameter(const char* name, PyObject* value); 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /sdk/tests/pki/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+TCCAeGgAwIBAgIJAIF42K0qiqRiMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV 3 | BAMMB2xvY2FsY2EwIBcNMTYwMzA3MTc0MTA2WhgPMjExNjAyMTIxNzQxMDZaMBIx 4 | EDAOBgNVBAMMB2xvY2FsY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 5 | AQC56TRp5pdbdSL9Qjo4XiwwyI2ZF/QTy1Q9rxBA5vcdAXVInr/kwforOGAvlaUQ 6 | j7jWRg+9tKYOOJrAj57kSqlaeRdIRyE3zpeFKBjFZMbRZDqXLTGF83LFVBfchA0A 7 | rEui+/5ZSpsVU5k+/7dm6hMUejjRv72ze3OpxCUp1+sWj93mjFBCHpV37XlWSv3D 8 | 64DWq1V8y6NVLRU01yWXd7Wyl4x1mx6Qr/hLuEaoQ5HHCM6XHuSFD3kQpyhxpNxx 9 | SAXUvAtNt2l8SgeD9TuldUg1HpwKQD9j7Y1YQTrwLuvyIaG7QiTx4KGPSAiQy8Y8 10 | MkzasS64oH9jTeQv3DezCfldAgMBAAGjUDBOMB0GA1UdDgQWBBQ47O7MqlaUMmbj 11 | /hwheqIDBvkcqDAfBgNVHSMEGDAWgBQ47O7MqlaUMmbj/hwheqIDBvkcqDAMBgNV 12 | HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBvoStZltaAie6EliNHaUJuwHUy 13 | iYH96D6vu2hbT0ALStdkI/cpmcxqxcUWw83V9ecKp+JPUKvuhXQI9WSwrrT5ADW+ 14 | yOIZkmlMzX5SWkn5gjLYWGZNAWx8wbgLI6STCXHI+F6EHZcD8Bn6cJiayBOsVwZV 15 | e++6sR1iP5bq0OBiiDoUqPeoQpwuIUatpxmI6Km9KKjgIhGoxFl64nGPz6rdrDA/ 16 | 59iW1jy0b0pS3HyzkF06G8SiNTSy/9EW088AmIs3NhTFD6mG/RjrAxO1YshZNI9S 17 | g491iuwOffheGbgZhHcAwA6ByzzK2/8D4AbOefkAvhKkcrVjGpD4HhcxsamN 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /sdk/ext/xml.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright oVirt Authors 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 implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include "ov_xml_module.h" 20 | #include "ov_xml_reader.h" 21 | #include "ov_xml_writer.h" 22 | 23 | #if PY_MAJOR_VERSION >= 3 24 | PyObject* PyInit_xml(void) { 25 | #else 26 | void initxml(void) { 27 | #endif 28 | 29 | /* Define the module: */ 30 | ov_xml_module_define(); 31 | #if PY_MAJOR_VERSION >= 3 32 | if (ov_xml_module == NULL) { 33 | return NULL; 34 | } 35 | #endif 36 | 37 | /* Define the classes: */ 38 | ov_xml_reader_define(); 39 | ov_xml_writer_define(); 40 | 41 | #if PY_MAJOR_VERSION >= 3 42 | return ov_xml_module; 43 | #endif 44 | } 45 | -------------------------------------------------------------------------------- /generator/src/main/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /generator/src/main/java/org/ovirt/sdk/python/Main.java: -------------------------------------------------------------------------------- 1 | /* 2 | The oVirt Project - Ovirt Engine SDK 3 | 4 | Copyright oVirt Authors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package org.ovirt.sdk.python; 20 | 21 | import org.jboss.weld.environment.se.Weld; 22 | import org.jboss.weld.environment.se.WeldContainer; 23 | 24 | /** 25 | * This class is responsible for bootstrapping the CDI container, creating the application entry point and running it 26 | * with the command line arguments. 27 | */ 28 | public class Main { 29 | public static void main(String[] args) throws Exception { 30 | Weld weld = new Weld(); 31 | WeldContainer container = weld.initialize(); 32 | Tool tool = container.instance().select(Tool.class).get(); 33 | tool.run(args); 34 | weld.shutdown(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sdk/ext/ov_xml_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright oVirt Authors 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 implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include "ov_xml_module.h" 20 | 21 | /* Module: */ 22 | PyObject* ov_xml_module; 23 | 24 | #if PY_MAJOR_VERSION >= 3 25 | static struct PyModuleDef ov_xml_module_definition = { 26 | PyModuleDef_HEAD_INIT, 27 | /* m_name */ OV_XML_MODULE_NAME, 28 | /* m_doc */ 0, 29 | /* m_size */ 0, 30 | /* m_methods */ 0, 31 | /* m_reload */ 0, 32 | /* m_traverse */ 0, 33 | /* m_clear */ 0, 34 | /* m_free */ 0 35 | }; 36 | #endif 37 | 38 | void ov_xml_module_define(void) { 39 | #if PY_MAJOR_VERSION >= 3 40 | ov_xml_module = PyModule_Create(&ov_xml_module_definition); 41 | #else 42 | ov_xml_module = Py_InitModule(OV_XML_MODULE_NAME, NULL); 43 | #endif 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/build-on-pr.yml: -------------------------------------------------------------------------------- 1 | name: build-on-pr 2 | on: 3 | [pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - name: centos-stream-9 13 | container-name: el9stream 14 | 15 | env: 16 | LD_LIBRARY_PATH: /usr/local/opt/curl/lib:$LD_LIBRARY_PATH 17 | PYCURL_SSL_LIBRARY: openssl 18 | MAVEN_OPTS: >- 19 | --add-opens java.base/java.lang=ALL-UNNAMED 20 | --add-opens java.base/java.lang.reflect=ALL-UNNAMED 21 | ARTIFACTS_DIR: sdk/lib/ovirtsdk4/ 22 | 23 | container: 24 | image: quay.io/ovirt/buildcontainer:${{ matrix.container-name }} 25 | 26 | name: Build and test engine-sdk on ${{ matrix.name }} 27 | 28 | steps: 29 | - uses: ovirt/checkout-action@main 30 | 31 | - name: Enable EPEL 32 | run: dnf -y install epel-release 33 | 34 | - name: Install package dependencies 35 | run: dnf -y install maven python3-pip python3-flake8 openssl-devel libcurl-devel java-21-openjdk-devel 36 | 37 | - name: Run maven build 38 | run: mvn package -B 39 | 40 | - name: Upload full build folder 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: generated-ovirtsdk4 44 | path: ${{ env.ARTIFACTS_DIR }} 45 | -------------------------------------------------------------------------------- /sdk/tests/test_xml_writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from io import BytesIO 20 | from ovirtsdk4.xml import XmlWriter 21 | 22 | 23 | def make_buffer(): 24 | """ 25 | Creates an IO object to be used for writing. 26 | """ 27 | return BytesIO() 28 | 29 | 30 | def decode_buffer(buf): 31 | """ 32 | Extracts the text stored in the given bytes buffer and generates an 33 | Unicode string. 34 | """ 35 | return buf.getvalue().decode('utf-8') 36 | 37 | 38 | def test_write_string(): 39 | """ 40 | Checks that given an name and a value the `write_string` method 41 | generates the expected XML text. 42 | """ 43 | buf = make_buffer() 44 | writer = XmlWriter(buf) 45 | writer.write_element('value', 'myvalue') 46 | writer.flush() 47 | assert decode_buffer(buf) == 'myvalue' 48 | -------------------------------------------------------------------------------- /generator/src/main/java/org/ovirt/sdk/python/PythonGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | The oVirt Project - Ovirt Engine SDK 3 | 4 | Copyright oVirt Authors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package org.ovirt.sdk.python; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | 24 | import org.ovirt.api.metamodel.concepts.Model; 25 | 26 | /** 27 | * The interface to be implemented by code generators. The tool will locate all the code generators, and for each 28 | * of them it will set the output file and invoke the {@link #generate(Model)} method. No specific order will be 29 | * used when there are multiple generators. 30 | */ 31 | public interface PythonGenerator { 32 | /** 33 | * Set the directory were the output should be generated. 34 | * @param out the path to the output directory. 35 | */ 36 | void setOut(File out); 37 | 38 | /** 39 | * Generates the code for the given model. 40 | * @param model the model to be used to generate the code. 41 | * @throws IOException in case of I/O errors. 42 | */ 43 | void generate(Model model) throws IOException; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /generator/src/main/java/org/ovirt/sdk/python/PythonClassName.java: -------------------------------------------------------------------------------- 1 | /* 2 | The oVirt Project - Ovirt Engine SDK 3 | 4 | Copyright oVirt Authors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package org.ovirt.sdk.python; 20 | 21 | /** 22 | * This class represents the fully qualified name of a Python class, composed by the module name and the class. 23 | */ 24 | public class PythonClassName { 25 | private String moduleName; 26 | private String className; 27 | 28 | public String getModuleName() { 29 | return moduleName; 30 | } 31 | 32 | public void setModuleName(String newModuleName) { 33 | moduleName = newModuleName; 34 | } 35 | 36 | public String getClassName() { 37 | return className; 38 | } 39 | 40 | public void setClassName(String newClassName) { 41 | className = newClassName; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | StringBuilder buffer = new StringBuilder(); 47 | buffer.append(moduleName); 48 | buffer.append("."); 49 | buffer.append(className); 50 | return buffer.toString(); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /sdk/tests/pki/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAv9uB1WfDldE45uVbifbpWSbzs97wc3BEFeeNA4ixAzqIlwtB 3 | BEtiLgYOsIo2EaQq3OxON/pST1YYLuWkmBA3CaQNvM/F1Ay3OD5jsciUfyYQK5gv 4 | NsFXIr4U29yHZGqF+Q3USxw5hZBc8rh5Uu/CpnH27RWR9Wnf7pVFJYauWOERzcdy 5 | RxI2byAxPmtili4sBO4GH2I3Hz7kQL/T153ajAsV5qfbI5RIpjVhu8u7RwW2wOrl 6 | 5GEUlOsbO4byIB1Seilccx/vxYC1YiwFQPJaA29eHZwjCZEkmrbgYcilWIKb2g5o 7 | XvGuUfBn37VP+EoZVcSMI2XhcdV45G/vAWPWnQIDAQABAoIBAEuwjq0hbaDqVLeK 8 | 3q00MuUQH+7jUhtIvCOkIRa205bBfeeQ8gp+rvIXQ65UKHaXPK3BsC4XXGeeQJYn 9 | 1OKE6yGrjhviIFrC1Xmj63LU/fiXjKOWO3Ivnm+/a709aFCuUcJquUkUjoDp7jF3 10 | o62bl0BRD6iC7P1T0ptuWkQuuROosDXodNjroJ6kAtUIuuOzi669wTpFC14dLRf3 11 | pkI1iyKXIcWYXRWF0kJTb/HySk0UU4qdvoV2J5l99HdQhZw6a/8QZO9P5wSRyEzB 12 | OHgsIfu15hc0jmeeE5GfGVt7F1y0LXyZw5dbeYj7s9o3VEUjfjItK0r6KFYuicUk 13 | 1kmZ6EUCgYEA7VN3FNc5fBn6qbe95QMD3nTAiNjX0xBhL0UtwSsOd+aoG4JqaSsw 14 | N5R+4tPLmJ+BoEpNK/ymAR/jcpKzm88rNJNz3841q/tRTLEgP8bVzk4AVQ3UZ8ig 15 | kmFK8ttlAvppZnN+4FvKmk50Q5v+FAXdB/S6u3S9vZsBrciZcSGjZIMCgYEAzvQn 16 | U4MSAwkltnzjuyMoF1cmvzBRuA9L25EiJUUui+dbWbb8BSU5qUgTDROGTnHI740w 17 | PEZIPUEcjRdk8AcPuD0vBC/fYULxvpcwN1Q5D2YGoJxsPJp+LcSZPJn6xTZojmqJ 18 | 3aXpJ/NQSkRMUwCtLQv41u221J1rnxw1RfTmLl8CgYAV9KXMoMipqYGeF+iSej/u 19 | YaC6SE1XMmm7RMwh1cjl4MnmmZ8ckalJSwyeEXgBa6hDWvxeuGXnLrsNC3NgU78s 20 | gwOyTdJ7UanIzY4tOEjpaB/xvnDLFS19vVCAvTlQGDiOCNtRCEzrD50D8DeGRLCZ 21 | HtPzqa4wD1oNaMSBSdpi3wKBgQCnr7EFu9gmWY0TNlKX2T6s2tLsa0xrpQlEGW7f 22 | YBT9CzM7mEbQLH9yKJI3MDDM8ulrIK2KyS/TYiSuNdx1mGMmV3z3GYsYFdQnJ/3L 23 | dxTc40BPdy9EU7IVh4zaS7Gjhhhl/PFEhSBMXJwb8Qce4hdvvpmcHPTdhcgkHgkU 24 | bADuZQKBgQCy+98Z1AOh8ht8iS5EAewmU1eZhh2EALx4yoq55HhBR7wWc1g9f17m 25 | H0EAiDEFAJsd8dxM4uS+GyQ/imJGFKKJp8f2Xnvig0sEgEodNm2mRrsxi/kD6M6T 26 | 11zOJE8gKSAhOoMpEVcKg4XYVswxFJ6d63KmbC8qBboHVd4zNC+Jog== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /sdk/tests/pki/ugly.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCVMXkCYQnoBTxO 3 | IlnzzYTaKc5MFRQZNZhlAKm53I09W+ohbr91cZIYeQiUWOrc/PUqyXBasmxApUsh 4 | GfF6OEbOD40DzV7hqd20cHYU2+E+9Qius+Tzrxm9Y0/0PGPlJFsFnXtiPToA221u 5 | ZtVKdEz+HyxK/z4w3basuMWBOsEWyREkPQ9m1hLKzVsliNYWZj0CQaeBmo5t/Glf 6 | yRVm8srw1HMiul8VwlVhE1AjBfPXFDAH58Qgb416o56JGlBkQzlNJew+D2mQKx7N 7 | lueVjRu3mGTS9u1ac3pHSpvb9T5TyfxjCvWGizb+82qQYVE7WfJK0zyGCCwxHxam 8 | WC3ymKA5AgMBAAECggEADz7b98NEyOHoFK/r8z74KXcGLr4krKLObNvRfD2ubSu8 9 | VChGBlFRtZCAsPAZJ/Xtc67b/VioOT7q23oUNAIWMi5ioZXV8UdzcWHgnycM8GsH 10 | tRqK9d/ZyIGihPm614qMhpleJ17MU5QYraRyfUY/K+SxmQRWLZYqeaH0BM2lN5ey 11 | aJXLREjUpyPtpPPlshLVKTHrbP7keCIRjxYWvDnyz0xlXm2zQzreWJTew3oMcVtG 12 | ymkZ3CCWPZz5KYAJt/WshyAGqi3n+JG1H9u0FIXGbZZu278cWCIUDzX40kfo47ty 13 | v3iWf2yPYVjpbQWqja+69DPSO+qnKELIx3O4NKYtEQKBgQDEUztrdBY7sKHrxH72 14 | ySJb6W4f52UtlxL7BRYR9H0J4mOfudUblyArbzrMWtfxUxqcpgv15V7za8emlQQU 15 | qVdGBRKkDtu38/r8zyaDzh+atoWEjkz+wJylTTZls3yUFmCYKusYdvhLRoRaPIGW 16 | 4MpzLpkpZGSJjsuEwedxPk30pQKBgQDCir04rtFjbmXQhBwBP2g5KzW/OXwAldSc 17 | ERbd5aWwMHQ9m8WWBwThOLVo+OMNNA4PK5TsktCY51NH3beJ+qEpV0Sjze7Xx6AI 18 | PwJqa/VSstSM7bNe0semGcRk7+gzwLSQVdAzmq0mvrrf75p6gLMFtPLXxr3ul7Or 19 | MGBjsyslBQKBgHfrGy1scDQvlQgtMxxNCUa5FAI61kt9ryNTHQMEoufJt+6VlT1Q 20 | F19QhsrSZnrKt0OeDUo6u7/WQtJWzXJNabikWpmJVd5MEjAf3DfATP+0o2OvhApL 21 | 3qL9wc9nTh4qeQAZnxaHfOyF+0wfD0z4q9ClUvq8jsiTR28k/djnJLjdAoGBAK1F 22 | KuGIewCHfHFqqRLHacm5XaaSyYov6OyUH/zFJHy3u2CAFEzatZLvkkwLmRbSbU8/ 23 | ruXCob3+EuPoayeerdoHWyBWM8vGhheyHzGwNBFTLBLVR7RGIgIj4xNPxk4J7gi8 24 | FbNQqbXfnMwFOV6wsEJ99ukOn24ZebIYZVGHa/8NAoGBAIyEoyezSEg1xaPUSWQF 25 | UxIqSr/wYUg2c369WBpKW9b3uNC1mfxfVOljPbhUOTce36h7s/IRLD9NDsYX3pQC 26 | qFsuR/an+KEDKJ0gN4BmHnU6N9+8q5GpGEAVvxtilL82VJBYsA0TC8JKSlu9uhXG 27 | ZUKpXXXYSdtcnDsiKhVah5Xz 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /sdk/tests/pki/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9uT+7UljrtCLB 3 | AhvgRSpZHdoirK4+bILS8KMNSsjjjfv7vnVA2AkMPCfg3zmy0HwVpy3FRpYaQz9y 4 | gKuOmh5B04/QKcZ17HnT375NAzAeC70i9763zhXuV4BEVyTMBgnRV+lWku/CorqW 5 | 3CR03Hicm5sgL3HyjrnET7DhWmiN0u7sUU8XFGII0PAtlfhu0j+aPxt96HmqzgY0 6 | 6yMJFIXBJdGTSrauKSHh1nAhq6RltgP1ldNPxqZ6TvgZgGuy4BEtGCYFGd2x2l9D 7 | 5MviNeX/hSwNZM1vh21Z3jmcsUzR3XzIm3YzNKwzFiiwDXpUdn6bETpwKWCiBO0O 8 | M1VvwDu3AgMBAAECggEBAJduw5Xq9pq8H3lbC5EI4/JZx4Ehv7qHtixUcnDrnkkz 9 | TUv1C3YEecRQR6xPwKgfjMtjsz7hRnIT9xKX7VdXYIs+KG4IyiXZ6KvazPR/dOqm 10 | iALfKFVho1Ood/MUc4R91VxJBTBa/BCo/rHnaRn12Dd4ZGteM19d2Jz+1ropBYcc 11 | 0mlLHYvTdP9IO1J+2h3UmZZFpcvZmSdSlWl14thnzIrDRqVnUDPNk8sHPfjaCzEa 12 | Zb7UAH438C6qFHzVVzvJ26cqS7KYfohE+1f/vAApA+K4Y7VX2l5YSyKN2ljcMERQ 13 | agdykkLhWlh4ztHqqq6Cy5yQhyJBxILJAF1yxKb/xfECgYEA9Je6kjW6YyN44pf4 14 | /OaRJcXKdhAi8DGG9nd4T/hldpgit+2RU3ThGGbZ+hAKgejpWDXTs77VeWXd1fNC 15 | sdnrCWecmasXg0aeAuaELhG+Q9poRdSDcEhKWsGngvtBffrBYQrV4qVTenZ7UnKd 16 | JLSOyqAB/bzLhhUyleIur+G9YysCgYEAxpJrsl0TArCe7vHuUPJyzmQa7k0r4haH 17 | m8OeR+QWYmmaS5dYmRIfGLyUN1khFyhLKhB/xm11jd5VayHvcVp+TpnKRG+truTI 18 | eaAf/NM2dMeg07b6jyD7HXv4mHiM69HKrBKrEos6o5beARZjUtRPwa/PaY3nfu78 19 | ZKDC81Hpc6UCgYB2b3oCDk4gby672gbQvvyNo8azgIDKedD2S0dQweCvml9FXJ3A 20 | IZpVbIgkE9xip1tGQVovcTqBPBg83zvuTq0GsssbhcMu5+TfVquuexz8UienmI3E 21 | stx+McNhIzTFQcSdrtd+lbtkUzbH54O8IEn8R5pvORn75QvHk+wzckV4XQKBgA9p 22 | yCBFJzebPAryDnyMBStOC+UZamGXPBl0GrIb2zzyU36wlbjz9iP2Z07QhUgF4ae3 23 | NiPR1UEY0+qH7M0QqCMzvsaHIKUlrwX5zuHSBzUTVcF5P4OinLtSJx62pMGdPC0V 24 | GeBLnFacXEkbUsRYJIS1P9VCpYhtxnuNGvTGE+fdAoGADdazRIIz9ts+1lq5RWZP 25 | JI2cRcWh1ftx5jwrcv7IaPBGU9VB+qVrb1yJDNa/ITnAFYQusy/jNVAlLPVQ59Sq 26 | gIjgIVeQg07ElidcrrkTj+E1BXuPles29ePvCVz89+XPQnAWahklyuugEUIAfyH9 27 | V3BnGenWkHns5rI31w4PkDQ= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /sdk/tests/test_sso_writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4.types as types 20 | 21 | from io import BytesIO 22 | from ovirtsdk4.writers import SsoWriter 23 | from ovirtsdk4.xml import XmlWriter 24 | 25 | 26 | def make_buffer(): 27 | """ 28 | Creates an IO object to be used for writing. 29 | """ 30 | return BytesIO() 31 | 32 | 33 | def decode_buffer(io_buffer): 34 | """ 35 | Extracts the text stored in the given bytes buffer and generates an 36 | Unicode string. 37 | """ 38 | return io_buffer.getvalue().decode('utf-8') 39 | 40 | 41 | def test_sso_method_id_is_attribute(): 42 | """ 43 | Test that writing an SSL object with one method writes the method 44 | identifier as the 'id' attribute. 45 | """ 46 | sso = types.Sso( 47 | methods=[ 48 | types.Method( 49 | id=types.SsoMethod.GUEST_AGENT 50 | ) 51 | ] 52 | ) 53 | buf = make_buffer() 54 | writer = XmlWriter(buf, indent=True) 55 | SsoWriter.write_one(sso, writer) 56 | writer.flush() 57 | assert ( 58 | decode_buffer(buf) == ( 59 | '\n' 60 | ' \n' 61 | ' \n' 62 | ' \n' 63 | '\n' 64 | ) 65 | ) 66 | -------------------------------------------------------------------------------- /sdk/lib/ovirtsdk4/http.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | 20 | class Request(object): 21 | """ 22 | This class represents an HTTP request. 23 | 24 | This class is intended for internal use by other components of the SDK. 25 | Refrain from using it directly as there is no backwards compatibility 26 | guarantee. 27 | """ 28 | 29 | def __init__( 30 | self, 31 | method='GET', 32 | path='', 33 | query=None, 34 | headers=None, 35 | body=None, 36 | ): 37 | self.method = method 38 | self.path = path 39 | self.query = query if query is not None else {} 40 | self.headers = headers if headers is not None else {} 41 | self.body = body 42 | 43 | 44 | class Response(object): 45 | """ 46 | This class represents an HTTP response. 47 | 48 | This class is intended for internal use by other components of the SDK. 49 | Refrain from using it directly as there is no backwards compatibility 50 | guarantee. 51 | """ 52 | 53 | def __init__( 54 | self, 55 | body=None, 56 | code=None, 57 | headers=None, 58 | message=None 59 | ): 60 | self.body = body 61 | self.code = code 62 | self.headers = headers if headers is not None else {} 63 | self.message = message 64 | -------------------------------------------------------------------------------- /sdk/tests/test_iscsi_discover.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4.types as types 20 | import unittest 21 | 22 | from .server import TestServer 23 | 24 | 25 | class IscsiDiscoverTest(unittest.TestCase): 26 | 27 | @classmethod 28 | def setup_class(cls): 29 | cls.server = TestServer() 30 | cls.server.start_server() 31 | cls.connection = cls.server.connection() 32 | 33 | @classmethod 34 | def teardown_class(cls): 35 | cls.connection.close() 36 | cls.server.stop_server() 37 | 38 | def test_action_parameters(self): 39 | """ 40 | Test if action parameters are constructed in correct way. 41 | """ 42 | self.server.set_xml_response("hosts/123/iscsidiscover", 200, "") 43 | hosts_service = self.connection.system_service().hosts_service() 44 | host_service = hosts_service.host_service('123') 45 | host_service.iscsi_discover( 46 | iscsi=types.IscsiDetails( 47 | address='iscsi.example.com', 48 | port=3260, 49 | ), 50 | ) 51 | assert ( 52 | self.server.last_request_content == ( 53 | "" 54 | "" 55 | "
iscsi.example.com
" 56 | "3260" 57 | "
" 58 | "
" 59 | ) 60 | ) 61 | -------------------------------------------------------------------------------- /sdk/tests/test_cluster_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4.types as types 20 | 21 | from io import BytesIO 22 | from ovirtsdk4.readers import ClusterReader 23 | from ovirtsdk4.xml import XmlReader 24 | 25 | 26 | def make_buffer(str): 27 | """ 28 | Creates an IO object to be used for writing. 29 | """ 30 | return BytesIO(str.encode('utf-8')) 31 | 32 | 33 | def test_cluster_with_no_rng_sources_and_switch_type(): 34 | """ 35 | Test that reading the 'switch_type' enum when it appears after an 36 | empty list works correctly. 37 | """ 38 | reader = XmlReader(make_buffer( 39 | '' 40 | '' 41 | 'legacy' 42 | '' 43 | )) 44 | result = ClusterReader.read_one(reader) 45 | reader.close() 46 | 47 | assert isinstance(result, types.Cluster) 48 | assert result.required_rng_sources == [] 49 | assert result.switch_type == types.SwitchType.LEGACY 50 | 51 | 52 | def test_unsupported_switch_type_dont_raise_exception(): 53 | """ 54 | Test when given switch type is unsupported, it don't raise exception. 55 | """ 56 | reader = XmlReader(make_buffer( 57 | '' 58 | 'ugly' 59 | '' 60 | )) 61 | result = ClusterReader.read_one(reader) 62 | reader.close() 63 | 64 | assert isinstance(result, types.Cluster) 65 | assert result.switch_type is None 66 | -------------------------------------------------------------------------------- /sdk/ext/ov_xml_utils.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright oVirt Authors 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 implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | 21 | #include "ov_xml_utils.h" 22 | 23 | xmlChar* 24 | ov_xml_get_string_parameter(const char* name, PyObject* value) { 25 | #if PY_MAJOR_VERSION >= 3 26 | #else 27 | PyObject* encoded = NULL; 28 | #endif 29 | xmlChar* result = NULL; 30 | 31 | #if PY_MAJOR_VERSION >= 3 32 | if (PyUnicode_Check(value)) { 33 | result = xmlCharStrdup(PyUnicode_AsUTF8(value)); 34 | if (result == NULL) { 35 | PyErr_Format(PyExc_TypeError, "Can't allocate XML string"); 36 | return NULL; 37 | } 38 | return result; 39 | } 40 | #else 41 | if (PyString_Check(value)) { 42 | result = xmlCharStrdup(PyString_AsString(value)); 43 | if (result == NULL) { 44 | PyErr_Format(PyExc_TypeError, "Can't allocate XML string"); 45 | return NULL; 46 | } 47 | return result; 48 | } 49 | if (PyUnicode_Check(value)) { 50 | encoded = PyUnicode_AsUTF8String(value); 51 | if (encoded == NULL) { 52 | return NULL; 53 | } 54 | result = xmlCharStrdup(PyString_AsString(encoded)); 55 | Py_DECREF(encoded); 56 | if (result == NULL) { 57 | PyErr_Format(PyExc_TypeError, "Can't allocate XML string"); 58 | return NULL; 59 | } 60 | return result; 61 | } 62 | #endif 63 | PyErr_Format(PyExc_TypeError, "The '%s' parameter must be a string", name); 64 | return NULL; 65 | } 66 | -------------------------------------------------------------------------------- /sdk/tests/test_connection_error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4 as sdk 20 | import unittest 21 | import pytest 22 | 23 | from .server import TestServer 24 | 25 | 26 | class ConnectionErrorTest(unittest.TestCase): 27 | 28 | @classmethod 29 | def setup_class(cls): 30 | cls.server = TestServer() 31 | cls.server.start_server() 32 | 33 | @classmethod 34 | def teardown_class(cls): 35 | cls.server.stop_server() 36 | 37 | def test_incorrect_server_fqdn(self): 38 | """ 39 | Fail if server has incorrect FQDN. 40 | """ 41 | with pytest.raises(sdk.ConnectionError): 42 | connection = sdk.Connection( 43 | username=self.server.user(), 44 | password=self.server.password(), 45 | insecure=True, 46 | url='https://300.300.300.300/ovirt-engine/api', 47 | ) 48 | connection.test(raise_exception=True) 49 | connection.close() 50 | 51 | def test_incorrect_server_address(self): 52 | """ 53 | Fail if server has incorrect address. 54 | """ 55 | with pytest.raises(sdk.ConnectionError): 56 | connection = sdk.Connection( 57 | username=self.server.user(), 58 | password=self.server.password(), 59 | insecure=True, 60 | url='https://bad.host/ovirt-engine/api', 61 | ) 62 | connection.test(raise_exception=True) 63 | connection.close() 64 | -------------------------------------------------------------------------------- /sdk/tests/test_check_types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4.services as services 20 | import ovirtsdk4.types as types 21 | import unittest 22 | import pytest 23 | 24 | 25 | class CheckTypesTest(unittest.TestCase): 26 | 27 | def test_service_type_error(self): 28 | """ 29 | Test that calling a method with multiple wrong parameter types 30 | generates an informative exception. 31 | """ 32 | vm_service = services.VmService(None, None) 33 | with pytest.raises(TypeError) as context: 34 | vm_service.start( 35 | use_cloud_init='true', 36 | vm=types.Disk(), 37 | ) 38 | message = str(context.value) 39 | assert ( 40 | "The 'use_cloud_init' parameter should be of type 'bool', " 41 | "but it is of type 'str'" in 42 | message 43 | ) 44 | assert ( 45 | "The 'vm' parameter should be of type 'Vm', but it is of " 46 | "type 'Disk'" in 47 | message 48 | ) 49 | 50 | def test_locator_type_error(self): 51 | """ 52 | Test that calling a service locator with a wrong parameter type 53 | generates an informative exception. 54 | """ 55 | vms_service = services.VmsService(None, None) 56 | with pytest.raises(TypeError) as context: 57 | vms_service.vm_service(types.Vm()) 58 | message = str(context.value) 59 | assert ( 60 | "The 'id' parameter should be of type 'str', but it is of " 61 | "type 'Vm'." in 62 | message 63 | ) 64 | -------------------------------------------------------------------------------- /sdk/tests/test_affinity_group_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4.types as types 20 | 21 | from io import BytesIO 22 | from ovirtsdk4.readers import AffinityGroupReader 23 | from ovirtsdk4.xml import XmlReader 24 | 25 | 26 | def make_buffer(str): 27 | """ 28 | Creates an IO object to be used for writing. 29 | """ 30 | return BytesIO(str.encode('utf-8')) 31 | 32 | 33 | def test_affinity_group_reader_with_assigned_vms(): 34 | """ 35 | Test that reading of `vms` attribute of affinity group reads the `href` 36 | of the link as well as content of the `vms` element. 37 | """ 38 | reader = XmlReader(make_buffer( 39 | '' 40 | '' 41 | '' 42 | '' 43 | )) 44 | result = AffinityGroupReader.read_one(reader) 45 | reader.close() 46 | 47 | assert isinstance(result, types.AffinityGroup) 48 | assert len(result.vms) > 0 49 | assert result.vms.href == '/ovirt-engine/api/clusters/123/affinitygroups/456/vms' 50 | assert result.vms[0].id == '123' 51 | 52 | 53 | def test_affinity_group_reader_with_assigned_vms_no_order(): 54 | """ 55 | Test that reading of `vms` attribute of affinity group reads the `href` 56 | of the link as well as content of the `vms` element. Test it's 57 | correctly processed when link is provided after 'vms' element. 58 | """ 59 | reader = XmlReader(make_buffer( 60 | '' 61 | '' 62 | '' 63 | '' 64 | )) 65 | result = AffinityGroupReader.read_one(reader) 66 | reader.close() 67 | 68 | assert isinstance(result, types.AffinityGroup) 69 | assert len(result.vms) > 0 70 | assert result.vms.href == '/ovirt-engine/api/clusters/123/affinitygroups/456/vms' 71 | assert result.vms[0].id == '123' 72 | -------------------------------------------------------------------------------- /sdk/tests/test_cluster_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import unittest 20 | 21 | from .server import TestServer 22 | 23 | 24 | class ClusterServiceTest(unittest.TestCase): 25 | 26 | @classmethod 27 | def setup_class(cls): 28 | cls.server = TestServer() 29 | cls.server.start_server() 30 | cls.connection = cls.server.connection() 31 | system_service = cls.connection.system_service() 32 | cls.clusters_service = system_service.clusters_service() 33 | 34 | @classmethod 35 | def teardown_class(cls): 36 | cls.connection.close() 37 | cls.server.stop_server() 38 | 39 | def test_get_service(self): 40 | """ 41 | Check that reference to clusters service is not none 42 | """ 43 | assert self.clusters_service is not None 44 | 45 | def test_get_list_of_clusters(self): 46 | """ 47 | Test returning empty clusters list 48 | """ 49 | self.server.set_xml_response("clusters", 200, "") 50 | clusters = self.clusters_service.list() 51 | assert clusters is not None 52 | assert clusters == [] 53 | 54 | def test_get_list_of_clusters_with_search(self): 55 | """ 56 | Test returning empty clusters list 57 | """ 58 | self.server.set_xml_response("clusters", 200, "") 59 | clusters = self.clusters_service.list(search="name=ugly") 60 | assert clusters is not None 61 | assert clusters == [] 62 | 63 | def test_get_cluster_by_id(self): 64 | """ 65 | Test we don't get null cluster service for existing 66 | cluster id and correct object 67 | """ 68 | self.server.set_xml_response( 69 | path="clusters/123", 70 | code=200, 71 | body="testcluster" 72 | ) 73 | cluster = self.clusters_service.cluster_service("123").get() 74 | assert cluster.id == "123" 75 | assert cluster.name == "testcluster" 76 | -------------------------------------------------------------------------------- /.github/workflows/build-on-merge.yml: -------------------------------------------------------------------------------- 1 | name: build-on-merge 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - sdk_4.4 8 | tags: ['*'] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - name: centos-stream-9 19 | container-name: el9stream 20 | 21 | env: 22 | LD_LIBRARY_PATH: /usr/local/opt/curl/lib:$LD_LIBRARY_PATH 23 | PYCURL_SSL_LIBRARY: openssl 24 | MAVEN_OPTS: >- 25 | --add-opens java.base/java.lang=ALL-UNNAMED 26 | --add-opens java.base/java.lang.reflect=ALL-UNNAMED 27 | 28 | container: 29 | image: quay.io/ovirt/buildcontainer:${{ matrix.container-name }} 30 | 31 | name: Build and test engine-sdk on ${{ matrix.name }} 32 | 33 | steps: 34 | - uses: ovirt/checkout-action@main 35 | 36 | - name: Update packages 37 | run: dnf -y update 38 | 39 | - name: Enable EPEL 40 | run: dnf -y install epel-release 41 | 42 | - name: Install package dependencies 43 | run: dnf -y install maven python3-pip python3-flake8 openssl-devel libcurl-devel java-21-openjdk-devel 44 | 45 | - name: Set git defaults 46 | run: | 47 | git config --global user.name 'github-actions[bot]' 48 | git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' 49 | 50 | - name: Run maven build 51 | run: mvn package -B 52 | 53 | - name: Set FOLDER variable according to pushed branch 54 | run: | 55 | if [[ ${GITHUB_REF} == 'refs/heads/master' ]]; 56 | then 57 | echo "REMOTE_BRANCH=main" >> $GITHUB_ENV; 58 | else 59 | echo "REMOTE_BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV; 60 | fi 61 | 62 | - name: Check out target repository 63 | uses: ovirt/checkout-action@main 64 | with: 65 | repository: '${{ secrets.TARGET_REPO }}' 66 | ssh-key: '${{ secrets.DEPLOY_KEY }}' 67 | path: python-ovirt-engine-sdk4 68 | ref: '${{ env.REMOTE_BRANCH }}' 69 | 70 | - name: Copy SDK to repository 71 | run: | 72 | rm -rf sdk/target sdk/tests sdk/pom.xml 73 | cp -r sdk/* ./python-ovirt-engine-sdk4/ 74 | 75 | - name: Push changes to python-ovirt-engine-sdk4 76 | run: | 77 | cd python-ovirt-engine-sdk4 78 | if git status --porcelain 2>/dev/null| grep -E "^??|^M" 79 | then 80 | git add . 81 | git commit -am "Sync with upstream" 82 | git push 83 | fi 84 | cd .. 85 | -------------------------------------------------------------------------------- /generator/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | 23 | 4.0.0 24 | 25 | 26 | org.ovirt.engine.api 27 | python-sdk-parent 28 | 4.5.2-SNAPSHOT 29 | 30 | 31 | python-sdk-generator 32 | jar 33 | 34 | oVirt Python SDK Generator 35 | 36 | 37 | 38 | 39 | org.ovirt.engine.api 40 | metamodel-concepts 41 | ${metamodel.version} 42 | 43 | 44 | 45 | org.ovirt.engine.api 46 | metamodel-analyzer 47 | ${metamodel.version} 48 | 49 | 50 | 51 | org.ovirt.engine.api 52 | metamodel-tool 53 | ${metamodel.version} 54 | 55 | 56 | 57 | commons-cli 58 | commons-cli 59 | 1.3.1 60 | 61 | 62 | 63 | commons-io 64 | commons-io 65 | 2.14.0 66 | 67 | 68 | 69 | 70 | org.jboss.weld.se 71 | weld-se-shaded 72 | 3.1.9.Final 73 | 74 | 75 | org.slf4j 76 | * 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /sdk/tests/test_datacenter_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import unittest 20 | 21 | from .server import TestServer 22 | 23 | 24 | class DataCenterServiceTest(unittest.TestCase): 25 | 26 | @classmethod 27 | def setup_class(cls): 28 | cls.server = TestServer() 29 | cls.server.start_server() 30 | cls.connection = cls.server.connection() 31 | system_service = cls.connection.system_service() 32 | cls.data_centers_service = system_service.data_centers_service() 33 | 34 | @classmethod 35 | def teardown_class(cls): 36 | cls.connection.close() 37 | cls.server.stop_server() 38 | 39 | def test_get_service(self): 40 | """ 41 | Check that reference to data centers service is not none 42 | """ 43 | assert self.data_centers_service is not None 44 | 45 | def test_get_list_of_data_centers(self): 46 | """ 47 | Test returning empty data centers list 48 | """ 49 | self.server.set_xml_response("datacenters", 200, "") 50 | data_centers = self.data_centers_service.list() 51 | assert data_centers is not None 52 | assert data_centers == [] 53 | 54 | def test_get_list_of_data_centers_with_search(self): 55 | """ 56 | Test returning empty data centers list 57 | """ 58 | self.server.set_xml_response("datacenters", 200, "") 59 | data_centers = self.data_centers_service.list(search="name=ugly") 60 | assert data_centers is not None 61 | assert data_centers == [] 62 | 63 | def test_get_data_center_by_id(self): 64 | """ 65 | Test we don't get null data center service for existing 66 | data center id and correct object 67 | """ 68 | self.server.set_xml_response( 69 | path="datacenters/123", 70 | code=200, 71 | body="testdc" 72 | ) 73 | dc = self.data_centers_service.data_center_service("123").get() 74 | assert dc.id == "123" 75 | assert dc.name == "testdc" 76 | -------------------------------------------------------------------------------- /sdk/tests/test_storage_domain_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import unittest 20 | 21 | from .server import TestServer 22 | 23 | 24 | class StorageDomainServiceTest(unittest.TestCase): 25 | 26 | @classmethod 27 | def setup_class(cls): 28 | cls.server = TestServer() 29 | cls.server.start_server() 30 | cls.connection = cls.server.connection() 31 | system_service = cls.connection.system_service() 32 | cls.sd_service = system_service.storage_domains_service() 33 | 34 | @classmethod 35 | def teardown_class(cls): 36 | cls.connection.close() 37 | cls.server.stop_server() 38 | 39 | def test_get_service(self): 40 | """ 41 | Check that reference to storage domains service is not none 42 | """ 43 | assert self.sd_service is not None 44 | 45 | def test_get_list_of_storage_domains(self): 46 | """ 47 | Test returning empty storage domains list 48 | """ 49 | self.server.set_xml_response( 50 | "storagedomains", 200, "" 51 | ) 52 | storage_domains = self.sd_service.list() 53 | assert storage_domains is not None 54 | assert storage_domains == [] 55 | 56 | def test_get_list_of_storage_domains_with_search(self): 57 | """ 58 | Test returning empty storage domains list 59 | """ 60 | self.server.set_xml_response( 61 | "storagedomains", 200, "" 62 | ) 63 | storage_domains = self.sd_service.list(search="name=ugly") 64 | assert storage_domains is not None 65 | assert storage_domains == [] 66 | 67 | def test_get_storage_domain_by_id(self): 68 | """ 69 | Test we don't get null storage domain service for existing 70 | storage domain id and correct object 71 | """ 72 | self.server.set_xml_response( 73 | path="storagedomains/123", 74 | code=200, 75 | body="testsd" 76 | ) 77 | dc = self.sd_service.storage_domain_service("123").get() 78 | assert dc.id == "123" 79 | assert dc.name == "testsd" 80 | -------------------------------------------------------------------------------- /generator/src/main/java/org/ovirt/sdk/python/PythonReservedWords.java: -------------------------------------------------------------------------------- 1 | /* 2 | The oVirt Project - Ovirt Engine SDK 3 | 4 | Copyright oVirt Authors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package org.ovirt.sdk.python; 20 | 21 | import java.util.Collections; 22 | import java.util.HashSet; 23 | import java.util.Set; 24 | import javax.annotation.PostConstruct; 25 | import javax.enterprise.inject.Produces; 26 | import javax.inject.Singleton; 27 | 28 | import org.ovirt.api.metamodel.tool.ReservedWords; 29 | 30 | /** 31 | * This class is a producer of the set of Python reserved words. 32 | */ 33 | @Singleton 34 | public class PythonReservedWords { 35 | private Set words; 36 | 37 | @PostConstruct 38 | private void init() { 39 | // Create the set: 40 | words = new HashSet<>(); 41 | 42 | // Keywords: 43 | words.add("False"); 44 | words.add("None"); 45 | words.add("True"); 46 | words.add("and"); 47 | words.add("as"); 48 | words.add("assert"); 49 | words.add("async"); 50 | words.add("await"); 51 | words.add("break"); 52 | words.add("class"); 53 | words.add("continue"); 54 | words.add("def"); 55 | words.add("del"); 56 | words.add("elif"); 57 | words.add("else"); 58 | words.add("except"); 59 | words.add("finally"); 60 | words.add("for"); 61 | words.add("from"); 62 | words.add("global"); 63 | words.add("if"); 64 | words.add("import"); 65 | words.add("in"); 66 | words.add("is"); 67 | words.add("lambda"); 68 | words.add("nonlocal"); 69 | words.add("not"); 70 | words.add("or"); 71 | words.add("pass"); 72 | words.add("raise"); 73 | words.add("return"); 74 | words.add("try"); 75 | words.add("while"); 76 | words.add("with"); 77 | words.add("yield"); 78 | 79 | // Wrap the set so that it is unmodifiable: 80 | words = Collections.unmodifiableSet(words); 81 | } 82 | 83 | /** 84 | * Produces the set of Python reserved words. 85 | * @return a set filled with the reserved words in Python as strings. 86 | */ 87 | @Produces 88 | @ReservedWords(language = "python") 89 | public Set getWords() { 90 | return words; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /generator/src/main/java/org/ovirt/sdk/python/PythonTypeReference.java: -------------------------------------------------------------------------------- 1 | /* 2 | The oVirt Project - Ovirt Engine SDK 3 | 4 | Copyright oVirt Authors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package org.ovirt.sdk.python; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | /** 25 | * This class represents a reference to a Python type, including all the imports that are necessary to use it. For 26 | * example, if the type reference is {@code types.Vm} and the {@code types} prefix corresponds to the package 27 | * {@code ovirtsdk4.types} then the list of imports will contain {@code from ovirtsdk4 import types}. 28 | */ 29 | public class PythonTypeReference { 30 | private String text; 31 | private List imports = new ArrayList<>(1); 32 | 33 | /** 34 | * Get the text regarding the reference 35 | * @return a string with the text of the reference. 36 | */ 37 | public String getText() { 38 | return text; 39 | } 40 | 41 | /** 42 | * Set the text regarding the reference. 43 | * @param newText the new text of the reference. 44 | */ 45 | public void setText(String newText) { 46 | text = newText; 47 | } 48 | 49 | /** 50 | * Set the text regarding the reference using a class. 51 | * @param clazz the class of which to use the simple name as the text of the reference. 52 | */ 53 | public void setText(Class clazz) { 54 | text = clazz.getSimpleName(); 55 | } 56 | 57 | /** 58 | * Get the list of imports that are necessary to use this type reference. 59 | * @return a list of strings with the imports. 60 | */ 61 | public List getImports() { 62 | return new ArrayList<>(imports); 63 | } 64 | 65 | /** 66 | * Set the list of imports that are necessary to use this type reference. 67 | * @param newImports a list of strings with the imports. 68 | */ 69 | public void setImports(List newImports) { 70 | imports.clear(); 71 | imports.addAll(newImports); 72 | } 73 | 74 | /** 75 | * Add a new import to the existing list of imports that are necessary to use this type reference. 76 | * @param newImport a string with the new import. 77 | */ 78 | public void addImport(String newImport) { 79 | imports.add(newImport); 80 | } 81 | 82 | @Override 83 | public String toString() { 84 | return text; 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /sdk/tests/test_invalid_authentication.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4 as sdk 20 | import unittest 21 | import pytest 22 | 23 | from .server import TestServer 24 | 25 | 26 | class InvalidAuthTest(unittest.TestCase): 27 | 28 | @classmethod 29 | def setup_class(cls): 30 | cls.server = TestServer() 31 | cls.server.start_server() 32 | 33 | @classmethod 34 | def teardown_class(cls): 35 | cls.server.stop_server() 36 | 37 | def test_invalid_credentials_oauth(self): 38 | """ 39 | Test that proper JSON error message is returned when using invalid 40 | credentials with oauth. 41 | """ 42 | connection = self.server.connection() 43 | self.server.set_json_response( 44 | path='%s/sso/oauth/token' % self.server.prefix(), 45 | code=401, 46 | body={ 47 | 'error': 'access_denied', 48 | 'error_description': "Cannot authenticate user 'admin@internal': The username or password is incorrect..", 49 | }, 50 | ) 51 | with pytest.raises(sdk.AuthError) as ctx: 52 | connection.authenticate() 53 | message = str(ctx.value) 54 | assert ( 55 | "Error during SSO authentication access_denied : Cannot authenticate user 'admin@internal': " 56 | "The username or password is incorrect.." == message 57 | ) 58 | 59 | def test_invalid_credentials_openid(self): 60 | """ 61 | Test that proper JSON error message is returned when using invalid 62 | credentials with openid. 63 | """ 64 | connection = self.server.connection() 65 | self.server.set_json_response( 66 | path='%s/sso/oauth/token' % self.server.prefix(), 67 | code=401, 68 | body={ 69 | 'error': "Cannot authenticate user 'admin@internal': The username or password is incorrect..", 70 | 'error_code': 'access_denied', 71 | }, 72 | ) 73 | with pytest.raises(sdk.AuthError) as ctx: 74 | connection.authenticate() 75 | message = str(ctx.value) 76 | assert ( 77 | "Error during SSO authentication access_denied : Cannot authenticate user 'admin@internal': " 78 | "The username or password is incorrect.." == message 79 | ) 80 | -------------------------------------------------------------------------------- /sdk/tests/test_fault_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from io import BytesIO 20 | from ovirtsdk4 import types 21 | from ovirtsdk4 import readers 22 | from ovirtsdk4.xml import XmlReader 23 | 24 | 25 | def make_reader(text): 26 | """ 27 | Creates an IO objec that reads from the given text. 28 | """ 29 | return XmlReader(BytesIO(text.encode('utf-8'))) 30 | 31 | 32 | def test_read_one_with_empty_xml(): 33 | """ 34 | Checks that given an empty XML element the `read_one` method creates 35 | creates the expected fault. 36 | """ 37 | reader = make_reader('') 38 | result = readers.FaultReader.read_one(reader) 39 | reader.close() 40 | assert result is not None 41 | assert type(result) is types.Fault 42 | assert result.reason is None 43 | assert result.detail is None 44 | 45 | 46 | def test_read_one_with_reason_only(): 47 | """ 48 | Checks that given an an XML with only the reason element the 49 | `read_one` method creates creates the expected fault. 50 | """ 51 | reader = make_reader('myreason') 52 | result = readers.FaultReader.read_one(reader) 53 | reader.close() 54 | assert result is not None 55 | assert type(result) is types.Fault 56 | assert result.reason == 'myreason' 57 | assert result.detail is None 58 | 59 | 60 | def test_read_one_with_detail_only(): 61 | """ 62 | Checks that given an an XML with only the detail element the 63 | `read_one` method creates creates the expected fault. 64 | """ 65 | reader = make_reader('mydetail') 66 | result = readers.FaultReader.read_one(reader) 67 | reader.close() 68 | assert result is not None 69 | assert type(result) is types.Fault 70 | assert result.reason is None 71 | assert result.detail == 'mydetail' 72 | 73 | 74 | def test_read_one_with_reason_and_detail(): 75 | """ 76 | Checks that given an an XML with only the reason and deetail 77 | elements `read_one` method creates creates the expected fault. 78 | """ 79 | reader = make_reader(""" 80 | 81 | myreason 82 | mydetail 83 | 84 | """) 85 | result = readers.FaultReader.read_one(reader) 86 | reader.close() 87 | assert result is not None 88 | assert type(result) is types.Fault 89 | assert result.reason == 'myreason' 90 | assert result.detail == 'mydetail' 91 | -------------------------------------------------------------------------------- /sdk/tests/test_setupnetworks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4.types as types 20 | import unittest 21 | 22 | from .server import TestServer 23 | 24 | 25 | class SetupNetworksTest(unittest.TestCase): 26 | 27 | @classmethod 28 | def setup_class(cls): 29 | cls.server = TestServer() 30 | cls.server.start_server() 31 | cls.connection = cls.server.connection() 32 | 33 | @classmethod 34 | def teardown_class(cls): 35 | cls.connection.close() 36 | cls.server.stop_server() 37 | 38 | def test_action_parameters(self): 39 | """ 40 | Test if action parameters are constructed in correct way. 41 | """ 42 | self.server.set_xml_response("hosts/123/setupnetworks", 200, "") 43 | hosts_service = self.connection.system_service().hosts_service() 44 | host_service = hosts_service.host_service('123') 45 | host_service.setup_networks( 46 | modified_bonds=[ 47 | types.HostNic( 48 | name='bond0', 49 | bonding=types.Bonding( 50 | options=[ 51 | types.Option( 52 | name="mode", 53 | type="4", 54 | ), 55 | ], 56 | slaves=[ 57 | types.HostNic( 58 | name='eth1', 59 | ), 60 | types.HostNic( 61 | name='eth2', 62 | ), 63 | ], 64 | ), 65 | ), 66 | ] 67 | ) 68 | assert ( 69 | self.server.last_request_content == ( 70 | "" 71 | "" 72 | "" 73 | "" 74 | "" 75 | "" 79 | "" 80 | "" 81 | "" 82 | "eth1" 83 | "" 84 | "" 85 | "eth2" 86 | "" 87 | "" 88 | "" 89 | "bond0" 90 | "" 91 | "" 92 | "" 93 | ) 94 | ) 95 | -------------------------------------------------------------------------------- /sdk/tests/test_vm_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4.types as types 20 | 21 | from io import BytesIO 22 | from ovirtsdk4.readers import VmReader 23 | from ovirtsdk4.xml import XmlReader 24 | 25 | 26 | def make_buffer(text): 27 | """ 28 | Creates an IO object to be used for writing. 29 | """ 30 | text = text.encode('utf-8') 31 | return BytesIO(text) 32 | 33 | 34 | def test_reading_of_INHERITABLE_BOOLEAN_FALSE(): 35 | """ 36 | Test reading the InheritableBoolean enum false value. 37 | """ 38 | reader = XmlReader(make_buffer( 39 | '' 40 | '' 41 | 'false' 42 | '' 43 | '' 44 | )) 45 | result = VmReader.read_one(reader) 46 | reader.close() 47 | 48 | assert isinstance(result, types.Vm) 49 | assert result.migration.auto_converge == types.InheritableBoolean.FALSE 50 | 51 | 52 | def test_reading_of_INHERITABLE_BOOLEAN_TRUE(): 53 | """ 54 | Test reading the InheritableBoolean enum true value. 55 | """ 56 | reader = XmlReader(make_buffer( 57 | '' 58 | '' 59 | 'true' 60 | '' 61 | '' 62 | )) 63 | result = VmReader.read_one(reader) 64 | reader.close() 65 | 66 | assert isinstance(result, types.Vm) 67 | assert result.migration.auto_converge == types.InheritableBoolean.TRUE 68 | 69 | 70 | def test_reading_of_INHERITABLE_BOOLEAN_INHERIT(): 71 | """ 72 | Test reading the InheritableBoolean enum inherit value. 73 | """ 74 | reader = XmlReader(make_buffer( 75 | '' 76 | '' 77 | 'inherit' 78 | '' 79 | '' 80 | )) 81 | result = VmReader.read_one(reader) 82 | reader.close() 83 | 84 | assert isinstance(result, types.Vm) 85 | assert result.migration.auto_converge == types.InheritableBoolean.INHERIT 86 | 87 | 88 | def test_reading_of_INHERITABLE_BOOLEAN_unsupported_value(): 89 | """ 90 | Test reading the InheritableBoolean enum unsupported value return None. 91 | """ 92 | reader = XmlReader(make_buffer( 93 | '' 94 | '' 95 | 'ugly' 96 | '' 97 | '' 98 | )) 99 | result = VmReader.read_one(reader) 100 | reader.close() 101 | 102 | assert isinstance(result, types.Vm) 103 | assert result.migration.auto_converge is None 104 | 105 | 106 | def test_reading_name_with_accents(): 107 | """ 108 | Test that reading a VM that has a name with accents works correctly. 109 | """ 110 | reader = XmlReader(make_buffer( 111 | '' 112 | 'áéíóúÁÉÍÓÚ' 113 | '' 114 | )) 115 | result = VmReader.read_one(reader) 116 | reader.close() 117 | 118 | assert isinstance(result, types.Vm) 119 | assert result.name == 'áéíóúÁÉÍÓÚ' 120 | -------------------------------------------------------------------------------- /sdk/tests/test_network_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4.types as types 20 | 21 | from io import BytesIO 22 | from ovirtsdk4.readers import NetworkReader 23 | from ovirtsdk4.xml import XmlReader 24 | 25 | 26 | def make_buffer(str): 27 | """ 28 | Creates an IO object to be used for writing. 29 | """ 30 | return BytesIO(str.encode('utf-8')) 31 | 32 | 33 | def test_network_with_no_usages(): 34 | """ 35 | Test given network with no usages element, the usages attribute is None. 36 | """ 37 | reader = XmlReader(make_buffer('')) 38 | result = NetworkReader.read_one(reader) 39 | reader.close() 40 | 41 | assert result.usages is None 42 | 43 | 44 | def test_network_with_empty_usages(): 45 | """ 46 | Test given network with empty usages element, the usages attribute is empty list. 47 | """ 48 | reader = XmlReader(make_buffer('')) 49 | result = NetworkReader.read_one(reader) 50 | reader.close() 51 | 52 | assert isinstance(result.usages, list) 53 | assert len(result.usages) == 0 54 | 55 | 56 | def test_network_with_one_usages(): 57 | """ 58 | Test given network with no usages element, the usages attribute is None. 59 | """ 60 | reader = XmlReader( 61 | make_buffer( 62 | 'vm' 63 | ) 64 | ) 65 | result = NetworkReader.read_one(reader) 66 | reader.close() 67 | 68 | assert isinstance(result.usages, list) 69 | assert len(result.usages) == 1 70 | assert result.usages[0] == types.NetworkUsage.VM 71 | 72 | 73 | def test_network_with_two_usages(): 74 | """ 75 | Test given network with no usages element, the usages attribute is None. 76 | """ 77 | reader = XmlReader( 78 | make_buffer( 79 | '' 80 | '' 81 | 'vm' 82 | 'display' 83 | '' 84 | '' 85 | ) 86 | ) 87 | result = NetworkReader.read_one(reader) 88 | reader.close() 89 | 90 | assert isinstance(result.usages, list) 91 | assert len(result.usages) == 2 92 | assert result.usages[0] == types.NetworkUsage.VM 93 | assert result.usages[1] == types.NetworkUsage.DISPLAY 94 | 95 | 96 | def test_unsupported_usage_dont_raise_exception(): 97 | """ 98 | Test when given network with unsupported usage element, 99 | it don't raise exception. 100 | """ 101 | reader = XmlReader( 102 | make_buffer( 103 | '' 104 | '' 105 | 'ugly' 106 | 'display' 107 | '' 108 | '' 109 | ) 110 | ) 111 | result = NetworkReader.read_one(reader) 112 | reader.close() 113 | 114 | assert isinstance(result.usages, list) 115 | assert len(result.usages) == 2 116 | assert result.usages[0] is None 117 | assert result.usages[1] == types.NetworkUsage.DISPLAY 118 | -------------------------------------------------------------------------------- /sdk/tests/test_network_writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4.types as types 20 | 21 | from io import BytesIO 22 | from ovirtsdk4.writers import NetworkWriter 23 | from ovirtsdk4.xml import XmlWriter 24 | 25 | 26 | def make_buffer(): 27 | """ 28 | Creates an IO object to be used for writing. 29 | """ 30 | return BytesIO() 31 | 32 | 33 | def decode_buffer(io_buffer): 34 | """ 35 | Extracts the text stored in the given bytes buffer and generates an 36 | Unicode string. 37 | """ 38 | return io_buffer.getvalue().decode('utf-8') 39 | 40 | 41 | def test_write_network_with_no_usages(): 42 | """ 43 | Test given network with usages attribute set to None, 44 | the usages isn't written to output xml 45 | """ 46 | network = types.Network() 47 | buf = make_buffer() 48 | writer = XmlWriter(buf, indent=True) 49 | NetworkWriter.write_one(network, writer) 50 | writer.flush() 51 | assert decode_buffer(buf) == '\n' 52 | 53 | 54 | def test_write_network_with_empty_usages(): 55 | """ 56 | Test given network with usages attribute set empty list, 57 | the usages empty element is written to output xml 58 | """ 59 | network = types.Network( 60 | usages=[], 61 | ) 62 | buf = make_buffer() 63 | writer = XmlWriter(buf, indent=True) 64 | NetworkWriter.write_one(network, writer) 65 | writer.flush() 66 | assert ( 67 | decode_buffer(buf) == ( 68 | '\n' 69 | ' \n' 70 | '\n' 71 | ) 72 | ) 73 | 74 | 75 | def test_write_network_with_one_usages(): 76 | """ 77 | Test given network with usages attribute set list with one value, 78 | the usages element with one value is written to output xml 79 | """ 80 | network = types.Network( 81 | usages=[ 82 | types.NetworkUsage.VM 83 | ], 84 | ) 85 | buf = make_buffer() 86 | writer = XmlWriter(buf, indent=True) 87 | NetworkWriter.write_one(network, writer) 88 | writer.flush() 89 | assert ( 90 | decode_buffer(buf) == ( 91 | '\n' 92 | ' \n' 93 | ' vm\n' 94 | ' \n' 95 | '\n' 96 | ) 97 | ) 98 | 99 | 100 | def test_write_network_with_two_usages(): 101 | """ 102 | Test given network with usages attribute set list with one value, 103 | the usages element with one value is written to output xml 104 | """ 105 | network = types.Network( 106 | usages=[ 107 | types.NetworkUsage.VM, 108 | types.NetworkUsage.DISPLAY, 109 | ], 110 | ) 111 | buf = make_buffer() 112 | writer = XmlWriter(buf, indent=True) 113 | NetworkWriter.write_one(network, writer) 114 | writer.flush() 115 | assert ( 116 | decode_buffer(buf) == ( 117 | '\n' 118 | ' \n' 119 | ' vm\n' 120 | ' display\n' 121 | ' \n' 122 | '\n' 123 | ) 124 | ) 125 | -------------------------------------------------------------------------------- /sdk/tests/test_connection_create.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4 as sdk 20 | import unittest 21 | import pytest 22 | 23 | from .server import TestServer 24 | 25 | 26 | class ConnectionCreateTest(unittest.TestCase): 27 | 28 | @classmethod 29 | def setup_class(cls): 30 | cls.server = TestServer() 31 | cls.server.set_xml_response("", 200, "") 32 | cls.server.start_server() 33 | 34 | @classmethod 35 | def teardown_class(cls): 36 | cls.server.stop_server() 37 | 38 | def test_secure_mode_without_ca(self): 39 | """ 40 | Test connection can be created when no CA is provided 41 | """ 42 | with pytest.raises(sdk.Error): 43 | connection = sdk.Connection( 44 | url=self.server.url(), 45 | username=self.server.user(), 46 | password=self.server.password(), 47 | ca_file='ugly.pem' 48 | ) 49 | connection.authenticate() 50 | connection.close() 51 | 52 | def test_secure_mode_with_ca(self): 53 | """ 54 | Test no exception is thrown when CA is provided to connection 55 | """ 56 | connection = sdk.Connection( 57 | url=self.server.url(), 58 | username=self.server.user(), 59 | password=self.server.password(), 60 | ca_file=self.server.ca_file(), 61 | ) 62 | connection.authenticate() 63 | connection.close() 64 | 65 | def test_insecure_mode_without_ca(self): 66 | """ 67 | Test that CA isn't required in insecure mode 68 | """ 69 | connection = sdk.Connection( 70 | url=self.server.url(), 71 | username=self.server.user(), 72 | password=self.server.password(), 73 | insecure=True, 74 | ) 75 | connection.authenticate() 76 | connection.close() 77 | 78 | def test_kerberos_auth(self): 79 | """ 80 | Test creation of kerberos connection 81 | """ 82 | connection = sdk.Connection( 83 | url=self.server.url(), 84 | kerberos=True, 85 | ca_file=self.server.ca_file(), 86 | ) 87 | connection.authenticate() 88 | connection.close() 89 | 90 | def test_invalid_header(self): 91 | """ 92 | When invalid header is used Error should be raised 93 | """ 94 | with pytest.raises(sdk.Error): 95 | request = sdk.http.Request( 96 | method='GET', 97 | headers={'X-header': 'žčě'}, 98 | ) 99 | connection = self.server.connection() 100 | connection.send(request) 101 | 102 | def test_valid_header(self): 103 | """ 104 | When valid header is properly encoded and sent 105 | """ 106 | request = sdk.http.Request( 107 | method='GET', 108 | headers={ 109 | 'X-header1': u'ABCDEF123', 110 | 'X-header2': 'ABCDEF123', 111 | }, 112 | ) 113 | connection = self.server.connection() 114 | connection.send(request) 115 | connection.close() 116 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = oVirt Python SDK 2 | 3 | == Introduction 4 | 5 | The oVirt Python SDK is a Python package that simplyfies access to the 6 | oVirt Engine API. 7 | 8 | IMPORTANT: This document describes how to generate, build and test the 9 | SDK. If you are interested in how to use it read the `README.adoc` file 10 | in the `sdk` directory instead. 11 | 12 | == Building 13 | 14 | The SDK uses http://www.xmlsoft.org[libxml2] for parsing and rendering 15 | XML documents, and the part that interacts with this library is written 16 | in C. This means that before building you must make sure you have the C 17 | compiler and the required header and libraries files installed in your 18 | system. For example to install with the `dnf` package manager you will 19 | need to do the following: 20 | 21 | # dnf -y install \ 22 | gcc \ 23 | libxml2-devel \ 24 | python3-devel 25 | 26 | Most of the source code of the Python SDK is automatically generated 27 | from the API model. 28 | 29 | The code generator is a Java program that resides in the `generator` 30 | directory. This Java program will get the API model and the metamodel 31 | artifacts from the available Maven repositories. Because of that in 32 | order to build the Python SDK you need to install some additional 33 | dependencies. For example to install with the `dnf` package manager 34 | you will need to do the following: 35 | 36 | # dnf -y install \ 37 | git \ 38 | maven-openjdk21 \ 39 | maven-local-openjdk21 \ 40 | python3-flake8 \ 41 | python3-wheel 42 | 43 | To build and run the tests use the following commands: 44 | 45 | $ git clone git@github.com:oVirt/ovirt-engine-sdk.git 46 | $ mvn package 47 | 48 | To build without running the tests: 49 | 50 | $ mvn package -Dskipflake=true -DskipTests=true 51 | 52 | This will build the code generator, run it to generate the SDK for the 53 | version of the API that corresponds to the branch of the SDK that you 54 | are using, and build the `.tar.gz` and `.whl` files. 55 | 56 | If you need to generate it for a different version of the API then you 57 | can use the `model.version` property. For example, if you need to 58 | generate the SDK for version `4.1.0` of the SDK you can use this 59 | command: 60 | 61 | $ mvn package -Dmodel.version=4.1.0 62 | 63 | By default the build and the tests are executed using the `python` command. 64 | If you wish to use a different version of Python you can use the 65 | `python.command` property: 66 | 67 | $ mvn package -Dpython.command=python3 68 | 69 | The generated `.tar.gz` and `.whl` files will be located in the 70 | `sdk/dist` directory: 71 | 72 | $ find sdk/dist 73 | sdk/dist/ovirt-engine-sdk-4.0.0a0.linux-x86_64.tar.gz 74 | sdk/dist/ovirt-engine-sdk-4.0.0a0.tar.gz 75 | sdk/dist/ovirt_engine_sdk-4.0.0a0-cp27-none-linux_x86_64.whl 76 | 77 | 78 | == Release 79 | 80 | To create a new release available on Maven Central one should provide a settings.xml file in the .m2 folder following the structure: 81 | [source, xml] 82 | ----- 83 | 84 | 85 | 86 | central 87 | ${mvn_central_generated_username} 88 | ${mvn_central_generated_pwd} 89 | 90 | 91 | 92 | 93 | central 94 | 95 | true 96 | 97 | 98 | gpg2 99 | ${gpg_passphrase} 100 | 101 | 102 | 103 | 104 | ---- 105 | 106 | After that all version tags need to be updated, that can be done using: 107 | 108 | $ mvn release:prepare 109 | 110 | NOTE: Snapshot versions do currently have a shelf life of 90 days on maven central. 111 | 112 | To start generating your release and pushing it towards maven central run: 113 | 114 | $ mvn deploy -Psign -------------------------------------------------------------------------------- /sdk/tests/pki/ugly.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 2 (0x2) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: CN=localca 7 | Validity 8 | Not Before: Mar 7 17:47:30 2016 GMT 9 | Not After : Feb 12 17:47:30 2116 GMT 10 | Subject: CN=ugly 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (2048 bit) 14 | Modulus: 15 | 00:95:31:79:02:61:09:e8:05:3c:4e:22:59:f3:cd: 16 | 84:da:29:ce:4c:15:14:19:35:98:65:00:a9:b9:dc: 17 | 8d:3d:5b:ea:21:6e:bf:75:71:92:18:79:08:94:58: 18 | ea:dc:fc:f5:2a:c9:70:5a:b2:6c:40:a5:4b:21:19: 19 | f1:7a:38:46:ce:0f:8d:03:cd:5e:e1:a9:dd:b4:70: 20 | 76:14:db:e1:3e:f5:08:ae:b3:e4:f3:af:19:bd:63: 21 | 4f:f4:3c:63:e5:24:5b:05:9d:7b:62:3d:3a:00:db: 22 | 6d:6e:66:d5:4a:74:4c:fe:1f:2c:4a:ff:3e:30:dd: 23 | b6:ac:b8:c5:81:3a:c1:16:c9:11:24:3d:0f:66:d6: 24 | 12:ca:cd:5b:25:88:d6:16:66:3d:02:41:a7:81:9a: 25 | 8e:6d:fc:69:5f:c9:15:66:f2:ca:f0:d4:73:22:ba: 26 | 5f:15:c2:55:61:13:50:23:05:f3:d7:14:30:07:e7: 27 | c4:20:6f:8d:7a:a3:9e:89:1a:50:64:43:39:4d:25: 28 | ec:3e:0f:69:90:2b:1e:cd:96:e7:95:8d:1b:b7:98: 29 | 64:d2:f6:ed:5a:73:7a:47:4a:9b:db:f5:3e:53:c9: 30 | fc:63:0a:f5:86:8b:36:fe:f3:6a:90:61:51:3b:59: 31 | f2:4a:d3:3c:86:08:2c:31:1f:16:a6:58:2d:f2:98: 32 | a0:39 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Basic Constraints: 36 | CA:FALSE 37 | Netscape Comment: 38 | OpenSSL Generated Certificate 39 | X509v3 Subject Key Identifier: 40 | 67:54:0B:FE:CD:48:90:20:E6:9A:9F:72:79:3D:A5:FD:C1:59:82:50 41 | X509v3 Authority Key Identifier: 42 | keyid:38:EC:EE:CC:AA:56:94:32:66:E3:FE:1C:21:7A:A2:03:06:F9:1C:A8 43 | 44 | Signature Algorithm: sha256WithRSAEncryption 45 | b1:2f:e3:3d:3c:08:71:6c:a1:32:6c:3f:0e:c1:a2:d3:73:bd: 46 | 0d:53:6a:34:68:e7:f5:27:af:58:4d:a7:c9:5a:0f:e0:4f:be: 47 | 92:3c:b1:0b:b5:1c:a7:85:47:17:f3:0c:f1:b3:a2:55:7d:7f: 48 | a0:35:86:3e:18:dc:ab:89:40:9a:99:c2:8b:aa:b7:d6:a1:f0: 49 | 75:ca:84:b1:42:24:a9:c4:44:ff:d6:20:6d:60:ae:48:82:62: 50 | 61:a6:73:44:0e:66:d3:5c:a3:05:17:b7:b0:ff:60:97:89:4a: 51 | 30:6f:c6:94:e8:68:70:17:47:f1:f6:aa:40:bb:e2:53:d8:b1: 52 | ac:f7:68:f2:cb:8e:f1:7d:a7:60:c3:77:4e:66:5b:04:85:e3: 53 | 12:21:54:7f:d5:e4:d5:50:2c:c4:7a:75:79:7b:a9:88:1e:1e: 54 | f8:8f:ac:03:2e:a4:1e:73:4e:b6:11:b5:4d:c4:f7:97:8a:2f: 55 | 7b:25:df:4f:2f:81:d9:34:5a:4c:d4:d2:e9:b8:a0:72:e1:64: 56 | da:65:d6:83:64:a6:5c:25:c6:fa:bf:1b:4f:78:fb:2e:e1:3f: 57 | 63:5e:a4:1b:ac:38:bf:90:59:93:52:46:b6:62:6f:a4:b9:5c: 58 | 3c:b5:eb:10:8c:1d:a9:09:b3:e4:da:d4:59:65:b8:68:a8:16: 59 | 1a:68:2b:d4 60 | -----BEGIN CERTIFICATE----- 61 | MIIDGTCCAgGgAwIBAgIBAjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdsb2Nh 62 | bGNhMCAXDTE2MDMwNzE3NDczMFoYDzIxMTYwMjEyMTc0NzMwWjAPMQ0wCwYDVQQD 63 | DAR1Z2x5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlTF5AmEJ6AU8 64 | TiJZ882E2inOTBUUGTWYZQCpudyNPVvqIW6/dXGSGHkIlFjq3Pz1KslwWrJsQKVL 65 | IRnxejhGzg+NA81e4andtHB2FNvhPvUIrrPk868ZvWNP9Dxj5SRbBZ17Yj06ANtt 66 | bmbVSnRM/h8sSv8+MN22rLjFgTrBFskRJD0PZtYSys1bJYjWFmY9AkGngZqObfxp 67 | X8kVZvLK8NRzIrpfFcJVYRNQIwXz1xQwB+fEIG+NeqOeiRpQZEM5TSXsPg9pkCse 68 | zZbnlY0bt5hk0vbtWnN6R0qb2/U+U8n8Ywr1hos2/vNqkGFRO1nyStM8hggsMR8W 69 | plgt8pigOQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVu 70 | U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUZ1QL/s1IkCDmmp9y 71 | eT2l/cFZglAwHwYDVR0jBBgwFoAUOOzuzKpWlDJm4/4cIXqiAwb5HKgwDQYJKoZI 72 | hvcNAQELBQADggEBALEv4z08CHFsoTJsPw7BotNzvQ1TajRo5/Unr1hNp8laD+BP 73 | vpI8sQu1HKeFRxfzDPGzolV9f6A1hj4Y3KuJQJqZwouqt9ah8HXKhLFCJKnERP/W 74 | IG1grkiCYmGmc0QOZtNcowUXt7D/YJeJSjBvxpToaHAXR/H2qkC74lPYsaz3aPLL 75 | jvF9p2DDd05mWwSF4xIhVH/V5NVQLMR6dXl7qYgeHviPrAMupB5zTrYRtU3E95eK 76 | L3sl308vgdk0WkzU0um4oHLhZNpl1oNkplwlxvq/G094+y7hP2NepBusOL+QWZNS 77 | RrZib6S5XDy16xCMHakJs+Ta1FlluGioFhpoK9Q= 78 | -----END CERTIFICATE----- 79 | -------------------------------------------------------------------------------- /sdk/tests/pki/localhost.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 1 (0x1) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: CN=localca 7 | Validity 8 | Not Before: Mar 7 17:47:03 2016 GMT 9 | Not After : Feb 12 17:47:03 2116 GMT 10 | Subject: CN=localhost 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (2048 bit) 14 | Modulus: 15 | 00:bd:b9:3f:bb:52:58:eb:b4:22:c1:02:1b:e0:45: 16 | 2a:59:1d:da:22:ac:ae:3e:6c:82:d2:f0:a3:0d:4a: 17 | c8:e3:8d:fb:fb:be:75:40:d8:09:0c:3c:27:e0:df: 18 | 39:b2:d0:7c:15:a7:2d:c5:46:96:1a:43:3f:72:80: 19 | ab:8e:9a:1e:41:d3:8f:d0:29:c6:75:ec:79:d3:df: 20 | be:4d:03:30:1e:0b:bd:22:f7:be:b7:ce:15:ee:57: 21 | 80:44:57:24:cc:06:09:d1:57:e9:56:92:ef:c2:a2: 22 | ba:96:dc:24:74:dc:78:9c:9b:9b:20:2f:71:f2:8e: 23 | b9:c4:4f:b0:e1:5a:68:8d:d2:ee:ec:51:4f:17:14: 24 | 62:08:d0:f0:2d:95:f8:6e:d2:3f:9a:3f:1b:7d:e8: 25 | 79:aa:ce:06:34:eb:23:09:14:85:c1:25:d1:93:4a: 26 | b6:ae:29:21:e1:d6:70:21:ab:a4:65:b6:03:f5:95: 27 | d3:4f:c6:a6:7a:4e:f8:19:80:6b:b2:e0:11:2d:18: 28 | 26:05:19:dd:b1:da:5f:43:e4:cb:e2:35:e5:ff:85: 29 | 2c:0d:64:cd:6f:87:6d:59:de:39:9c:b1:4c:d1:dd: 30 | 7c:c8:9b:76:33:34:ac:33:16:28:b0:0d:7a:54:76: 31 | 7e:9b:11:3a:70:29:60:a2:04:ed:0e:33:55:6f:c0: 32 | 3b:b7 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Basic Constraints: 36 | CA:FALSE 37 | Netscape Comment: 38 | OpenSSL Generated Certificate 39 | X509v3 Subject Key Identifier: 40 | DC:B1:D0:97:46:64:2C:9A:A9:F0:C2:BF:5A:A2:81:7D:5D:1E:53:40 41 | X509v3 Authority Key Identifier: 42 | keyid:38:EC:EE:CC:AA:56:94:32:66:E3:FE:1C:21:7A:A2:03:06:F9:1C:A8 43 | 44 | Signature Algorithm: sha256WithRSAEncryption 45 | 6a:84:6d:00:c2:47:c4:e2:81:f0:c3:e5:c9:2d:43:b4:8d:f8: 46 | 9c:37:d8:16:18:78:05:77:a7:fa:68:41:0b:c9:96:af:5d:20: 47 | d7:03:da:bb:dc:cd:63:ff:23:2e:af:70:4a:70:8a:ad:07:14: 48 | 18:4f:94:e9:29:81:1b:e0:4f:5a:e7:1d:64:34:98:8c:9b:06: 49 | 06:36:80:3b:25:03:02:94:b8:a5:21:9c:77:45:5b:06:ad:16: 50 | f3:dc:38:db:46:18:02:16:5c:f2:44:dd:c3:e6:d6:d4:59:0c: 51 | f6:76:70:f0:dd:02:40:f2:c5:06:d6:9c:32:b2:e6:44:10:b6: 52 | cb:1b:5e:28:6f:4f:5f:4e:f8:0a:0c:83:ae:8e:96:76:28:87: 53 | 39:41:b2:48:70:01:d0:f7:fc:c6:5b:85:d4:ad:fa:fa:46:6b: 54 | 5c:13:7a:7a:1b:c4:03:85:f4:47:44:69:c3:11:10:5c:25:c8: 55 | 68:7f:d1:94:50:f2:2b:13:37:62:8e:4c:56:32:d6:fd:78:fb: 56 | c9:c6:04:f4:8f:34:b9:4f:d5:3f:76:75:24:19:af:66:da:ef: 57 | 58:25:fe:44:5e:2f:62:d3:b8:41:8e:ac:15:66:e6:f2:27:f8: 58 | 18:12:be:c9:36:ca:73:74:1d:b2:7d:c4:14:29:33:25:6c:70: 59 | 75:79:ef:85 60 | -----BEGIN CERTIFICATE----- 61 | MIIDHjCCAgagAwIBAgIBATANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdsb2Nh 62 | bGNhMCAXDTE2MDMwNzE3NDcwM1oYDzIxMTYwMjEyMTc0NzAzWjAUMRIwEAYDVQQD 63 | DAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9uT+7 64 | UljrtCLBAhvgRSpZHdoirK4+bILS8KMNSsjjjfv7vnVA2AkMPCfg3zmy0HwVpy3F 65 | RpYaQz9ygKuOmh5B04/QKcZ17HnT375NAzAeC70i9763zhXuV4BEVyTMBgnRV+lW 66 | ku/CorqW3CR03Hicm5sgL3HyjrnET7DhWmiN0u7sUU8XFGII0PAtlfhu0j+aPxt9 67 | 6HmqzgY06yMJFIXBJdGTSrauKSHh1nAhq6RltgP1ldNPxqZ6TvgZgGuy4BEtGCYF 68 | Gd2x2l9D5MviNeX/hSwNZM1vh21Z3jmcsUzR3XzIm3YzNKwzFiiwDXpUdn6bETpw 69 | KWCiBO0OM1VvwDu3AgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8W 70 | HU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTcsdCXRmQs 71 | mqnwwr9aooF9XR5TQDAfBgNVHSMEGDAWgBQ47O7MqlaUMmbj/hwheqIDBvkcqDAN 72 | BgkqhkiG9w0BAQsFAAOCAQEAaoRtAMJHxOKB8MPlyS1DtI34nDfYFhh4BXen+mhB 73 | C8mWr10g1wPau9zNY/8jLq9wSnCKrQcUGE+U6SmBG+BPWucdZDSYjJsGBjaAOyUD 74 | ApS4pSGcd0VbBq0W89w420YYAhZc8kTdw+bW1FkM9nZw8N0CQPLFBtacMrLmRBC2 75 | yxteKG9PX074CgyDro6WdiiHOUGySHAB0Pf8xluF1K36+kZrXBN6ehvEA4X0R0Rp 76 | wxEQXCXIaH/RlFDyKxM3Yo5MVjLW/Xj7ycYE9I80uU/VP3Z1JBmvZtrvWCX+RF4v 77 | YtO4QY6sFWbm8if4GBK+yTbKc3Qdsn3EFCkzJWxwdXnvhQ== 78 | -----END CERTIFICATE----- 79 | -------------------------------------------------------------------------------- /sdk/tests/test_read_link.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4 as sdk 20 | import ovirtsdk4.xml as xml 21 | import ovirtsdk4.readers as readers 22 | import ovirtsdk4.types as types 23 | 24 | from io import BytesIO 25 | 26 | 27 | def make_reader(text): 28 | """ 29 | Creates an IO object that reads from the given text. 30 | """ 31 | return xml.XmlReader(BytesIO(text.encode('utf-8'))) 32 | 33 | 34 | def test_link_href(): 35 | """ 36 | Checks that given an link the corresponding attribute is populate 37 | """ 38 | reader = make_reader( 39 | '' 40 | '' 41 | '' 42 | ) 43 | result = readers.VmReader.read_one(reader) 44 | assert result is not None 45 | assert isinstance(result, types.Vm) 46 | assert result.nics is not None 47 | assert isinstance(result.nics, sdk.List) 48 | assert result.nics.href == '/vms/123/nics' 49 | 50 | 51 | def test_element_after_link(): 52 | """ 53 | Check that another attribute after link is read correctly 54 | """ 55 | reader = make_reader( 56 | '' 57 | '' 58 | 'myvm' 59 | '' 60 | ) 61 | result = readers.VmReader.read_one(reader) 62 | assert result is not None 63 | assert isinstance(result, types.Vm) 64 | assert result.name == 'myvm' 65 | 66 | 67 | def test_link_is_ignored_if_not_exists(): 68 | """ 69 | Check that the link is ignored if there is no such link 70 | """ 71 | reader = make_reader( 72 | '' 73 | '' 74 | '' 75 | ) 76 | result = readers.VmReader.read_one(reader) 77 | assert result is not None 78 | assert isinstance(result, types.Vm) 79 | assert result.nics is None 80 | 81 | 82 | def test_link_is_ignored_if_no_rel(): 83 | """ 84 | Check that the link is ignored if there is no rel 85 | """ 86 | reader = make_reader( 87 | '' 88 | '' 89 | '' 90 | ) 91 | result = readers.VmReader.read_one(reader) 92 | assert result is not None 93 | assert isinstance(result, types.Vm) 94 | assert result.nics is None 95 | 96 | 97 | def test_link_is_ignored_if_no_href(): 98 | """ 99 | Check that the link is ignored if there is no href 100 | """ 101 | reader = make_reader( 102 | '' 103 | '' 104 | '' 105 | ) 106 | result = readers.VmReader.read_one(reader) 107 | assert result is not None 108 | assert isinstance(result, types.Vm) 109 | assert result.nics is None 110 | 111 | 112 | def test_multiple_links(): 113 | """ 114 | Check that the multiple links are read correctly 115 | """ 116 | reader = make_reader( 117 | '' 118 | '' 119 | '' 120 | '' 121 | ) 122 | result = readers.VmReader.read_one(reader) 123 | assert result is not None 124 | assert isinstance(result, types.Vm) 125 | assert result.nics is not None 126 | assert isinstance(result.nics, sdk.List) 127 | assert result.nics.href == '/vms/123/nics' 128 | assert result.cdroms is not None 129 | assert isinstance(result.cdroms, sdk.List) 130 | assert result.cdroms.href == '/vms/123/cdroms' 131 | 132 | 133 | def test_attribute_after_multiple_links(): 134 | """ 135 | Check when the multiple links, following attribute is populated correctly 136 | """ 137 | reader = make_reader( 138 | '' 139 | '' 140 | '' 141 | 'myvm' 142 | '' 143 | ) 144 | result = readers.VmReader.read_one(reader) 145 | assert result is not None 146 | assert isinstance(result, types.Vm) 147 | assert result.name == 'myvm' 148 | -------------------------------------------------------------------------------- /sdk/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | 23 | 4.0.0 24 | 25 | 26 | org.ovirt.engine.api 27 | python-sdk-parent 28 | 4.5.2-SNAPSHOT 29 | 30 | 31 | python-sdk 32 | pom 33 | 34 | oVirt Python SDK 35 | 36 | 37 | 38 | 41 | ${project.version} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | org.apache.maven.plugins 52 | maven-dependency-plugin 53 | 54 | 55 | copy-model 56 | generate-sources 57 | 58 | copy 59 | 60 | 61 | 62 | 63 | org.ovirt.engine.api 64 | model 65 | ${model.version} 66 | jar 67 | sources 68 | ${project.basedir}/target 69 | model.jar 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | org.codehaus.mojo 79 | exec-maven-plugin 80 | 3.0.0 81 | 82 | 83 | 84 | 85 | generate-code 86 | generate-sources 87 | 88 | java 89 | 90 | 91 | org.ovirt.api.metamodel.tool.Main 92 | 93 | org.ovirt.sdk.python.Tool 94 | --model=${project.basedir}/target/model.jar 95 | --out=${project.basedir}/lib 96 | --version=${sdk.version} 97 | 98 | true 99 | false 100 | 101 | 102 | 103 | 104 | 105 | format-check 106 | package 107 | 108 | exec 109 | 110 | 111 | ${skipflake} 112 | flake8 113 | 114 | --ignore=E501 115 | lib 116 | 117 | 118 | 119 | 120 | 121 | 122 | ${project.groupId} 123 | python-sdk-generator 124 | ${project.version} 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /generator/src/main/java/org/ovirt/sdk/python/Tool.java: -------------------------------------------------------------------------------- 1 | /* 2 | The oVirt Project - Ovirt Engine SDK 3 | 4 | Copyright oVirt Authors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package org.ovirt.sdk.python; 20 | 21 | import java.io.File; 22 | import javax.enterprise.context.ApplicationScoped; 23 | import javax.enterprise.inject.Any; 24 | import javax.enterprise.inject.Instance; 25 | import javax.inject.Inject; 26 | 27 | import org.apache.commons.cli.CommandLine; 28 | import org.apache.commons.cli.CommandLineParser; 29 | import org.apache.commons.cli.DefaultParser; 30 | import org.apache.commons.cli.HelpFormatter; 31 | import org.apache.commons.cli.Option; 32 | import org.apache.commons.cli.Options; 33 | import org.apache.commons.cli.ParseException; 34 | import org.apache.commons.io.FileUtils; 35 | import org.ovirt.api.metamodel.analyzer.ModelAnalyzer; 36 | import org.ovirt.api.metamodel.concepts.Model; 37 | import org.ovirt.api.metamodel.tool.BuiltinTypes; 38 | 39 | /** 40 | * Class that is used to run the Python generators. 41 | */ 42 | @ApplicationScoped 43 | public class Tool { 44 | // The names of the command line options: 45 | private static final String MODEL_OPTION = "model"; 46 | private static final String OUT_OPTION = "out"; 47 | private static final String VERSION_OPTION = "version"; 48 | 49 | // Reference to the objects used to calculate Python names: 50 | @Inject private PythonNames pythonNames; 51 | 52 | // References to the generators: 53 | @Inject @Any 54 | private Instance generators; 55 | 56 | // Reference to the object used to add built-in types to the model: 57 | @Inject private BuiltinTypes builtinTypes; 58 | 59 | public void run(String[] args) throws Exception { 60 | // Create the command line options: 61 | Options options = new Options(); 62 | 63 | // Options for the locations of files and directories: 64 | options.addOption(Option.builder() 65 | .longOpt(MODEL_OPTION) 66 | .desc("The directory or .jar file containing the source model files.") 67 | .type(File.class) 68 | .required(true) 69 | .hasArg(true) 70 | .argName("DIRECTORY|JAR") 71 | .build() 72 | ); 73 | 74 | // Options for the location of the generated Python sources: 75 | options.addOption(Option.builder() 76 | .longOpt(OUT_OPTION) 77 | .desc("The directory where the generated Python source will be created.") 78 | .type(File.class) 79 | .required(false) 80 | .hasArg(true) 81 | .argName("DIRECTORY") 82 | .build() 83 | ); 84 | 85 | // Option to specify the version number: 86 | options.addOption(Option.builder() 87 | .longOpt(VERSION_OPTION) 88 | .desc("The the version number of the SDK, for example \"4.0.0.a0\".") 89 | .type(File.class) 90 | .required(true) 91 | .hasArg(true) 92 | .argName("VERSION") 93 | .build() 94 | ); 95 | 96 | // Parse the command line: 97 | CommandLineParser parser = new DefaultParser(); 98 | CommandLine line = null; 99 | try { 100 | line = parser.parse(options, args); 101 | } 102 | catch (ParseException exception) { 103 | HelpFormatter formatter = new HelpFormatter(); 104 | formatter.setSyntaxPrefix("Usage: "); 105 | formatter.printHelp("python-tool [OPTIONS]", options); 106 | System.exit(1); 107 | } 108 | 109 | // Extract the locations of files and directories from the command line: 110 | File modelFile = (File) line.getParsedOptionValue(MODEL_OPTION); 111 | File outDir = (File) line.getParsedOptionValue(OUT_OPTION); 112 | 113 | // Extract the version number: 114 | String version = line.getOptionValue(VERSION_OPTION); 115 | 116 | // The version will usually come from the root POM of the project, where it will the "-SNAPSHOT" suffix for non 117 | // release versions. We need to remove that suffix. 118 | version = version.replaceAll("-SNAPSHOT$", ""); 119 | 120 | // Analyze the model files: 121 | Model model = new Model(); 122 | ModelAnalyzer modelAnalyzer = new ModelAnalyzer(); 123 | modelAnalyzer.setModel(model); 124 | modelAnalyzer.analyzeSource(modelFile); 125 | 126 | // Add the built-in types: 127 | builtinTypes.addBuiltinTypes(model); 128 | 129 | // Configure the object used to generate names: 130 | pythonNames.setVersion(version); 131 | 132 | // Run the generators: 133 | if (outDir != null) { 134 | FileUtils.forceMkdir(outDir); 135 | for (PythonGenerator generator : generators) { 136 | generator.setOut(outDir); 137 | generator.generate(model); 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /sdk/tests/test_xml_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from io import BytesIO 20 | from ovirtsdk4.xml import XmlReader 21 | 22 | 23 | def make_reader(text): 24 | """ 25 | Creates an IO objec that reads from the given text. 26 | """ 27 | text = text.encode('utf-8') 28 | return XmlReader(BytesIO(text)) 29 | 30 | 31 | def test_get_attribute_with_value(): 32 | """ 33 | Checks that given an attribute with a value the `get_attribute` 34 | method returns the value. 35 | """ 36 | reader = make_reader('') 37 | assert reader.get_attribute('id') == '123' 38 | 39 | 40 | def test_get_empty_attribute(): 41 | """ 42 | Checks that given an empty attribute the `get_attribute` method 43 | returns an empty string. 44 | """ 45 | reader = make_reader('') 46 | assert reader.get_attribute('id') == '' 47 | 48 | 49 | def test_get_non_existent_attribute(): 50 | """ 51 | Checks that given a non existing attribute the `get_attribute` 52 | method returns `None`. 53 | """ 54 | reader = make_reader('') 55 | assert reader.get_attribute('id') is None 56 | 57 | 58 | def test_read_empty_element(): 59 | """ 60 | Checks that given an empty element the `read_element` method 61 | returns `None`. 62 | """ 63 | reader = make_reader('') 64 | assert reader.read_element() is None 65 | 66 | 67 | def test_read_blank_element(): 68 | """ 69 | Checks that given an blank element the `read_element` method 70 | returns an empty string. 71 | """ 72 | reader = make_reader('') 73 | assert reader.read_element() == '' 74 | 75 | 76 | def test_read_empty_list(): 77 | """ 78 | Checks that given an empty element the `read_elements` method 79 | returns an empty list. 80 | """ 81 | reader = make_reader('') 82 | assert reader.read_elements() == [] 83 | 84 | 85 | def test_read_list_with_empty_element(): 86 | """ 87 | Checks that given a list with an empty element the `read_elements` method 88 | returns a list containing `None`. 89 | """ 90 | reader = make_reader('') 91 | assert reader.read_elements() == [None] 92 | 93 | 94 | def test_read_list_with_blank_element(): 95 | """ 96 | Checks that given a list with an blank element the `read_elements` method 97 | returns a list containing an empty string. 98 | """ 99 | reader = make_reader('') 100 | assert reader.read_elements() == [''] 101 | 102 | 103 | def test_read_list_one_element(): 104 | """ 105 | Checks that given a list with an one element the `read_elements` method 106 | returns a list containing it. 107 | """ 108 | reader = make_reader('first') 109 | assert reader.read_elements() == ['first'] 110 | 111 | 112 | def test_read_list_two_element(): 113 | """ 114 | Checks that given a list with an two elements the `read_elements` method 115 | returns a list containing them. 116 | """ 117 | reader = make_reader(""" 118 | 119 | first 120 | second 121 | 122 | """) 123 | assert reader.read_elements() == ['first', 'second'] 124 | 125 | 126 | def test_forward_with_preceding_test(): 127 | """ 128 | Checks that given some text before an element, the `forward` method 129 | skips the text and returns `True`. 130 | """ 131 | reader = make_reader('text') 132 | reader.read() 133 | assert reader.forward() 134 | assert reader.node_name() == 'target' 135 | 136 | 137 | def test_forward_end_of_document(): 138 | """ 139 | Checks that when positioned at the end of the document the `forward` 140 | method returns `False`. 141 | """ 142 | reader = make_reader('') 143 | reader.read() 144 | assert not reader.forward() 145 | 146 | 147 | def test_forward_with_empty_element(): 148 | """ 149 | Checks that when positioned at an empty element the `forward` method 150 | returns `True` and stays at the empty element. 151 | """ 152 | reader = make_reader('') 153 | reader.read() 154 | assert reader.forward() 155 | assert reader.node_name() == 'target' 156 | assert reader.empty_element() 157 | 158 | 159 | def test_read_element_after_empty_list(): 160 | """ 161 | Checks that given an empty list without a close element the 162 | `read_elements` method returns an empty list and the next element 163 | can be read with the `read_element` method. 164 | """ 165 | reader = make_reader('next') 166 | reader.read() 167 | assert reader.read_elements() == [] 168 | assert reader.read_element() == 'next' 169 | 170 | 171 | def test_read_accents(): 172 | """ 173 | Checks that reading text that is already encoded using UTF-8 174 | works correctly. 175 | """ 176 | reader = make_reader('áéíóúÁÉÍÓÚ') 177 | assert reader.read_element() == 'áéíóúÁÉÍÓÚ' 178 | -------------------------------------------------------------------------------- /sdk/tests/test_writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from io import BytesIO 20 | import re 21 | from ovirtsdk4 import Error 22 | from ovirtsdk4 import types 23 | from ovirtsdk4.writer import Writer 24 | from ovirtsdk4.xml import XmlWriter 25 | import pytest 26 | 27 | 28 | def make_buffer(): 29 | """ 30 | Creates an IO object to be used for writing. 31 | """ 32 | return BytesIO() 33 | 34 | 35 | def decode_buffer(io_buffer): 36 | """ 37 | Extracts the text stored in the given bytes buffer and generates an 38 | Unicode string. 39 | """ 40 | return io_buffer.getvalue().decode('utf-8') 41 | 42 | 43 | def test_write_string(): 44 | """ 45 | Checks that given an name and a value the `write_string` method 46 | generates the expected XML text. 47 | """ 48 | io_buffer = make_buffer() 49 | xml_writer = XmlWriter(io_buffer) 50 | Writer.write_string(xml_writer, 'value', 'myvalue') 51 | xml_writer.flush() 52 | assert decode_buffer(io_buffer) == 'myvalue' 53 | 54 | 55 | def test_write_boolean_true(): 56 | """ 57 | Checks that given the value `True` the `write_boolean` method generates 58 | the expected XML text. 59 | """ 60 | io_buffer = make_buffer() 61 | xml_writer = XmlWriter(io_buffer) 62 | Writer.write_boolean(xml_writer, 'value', True) 63 | xml_writer.flush() 64 | assert decode_buffer(io_buffer) == 'true' 65 | 66 | 67 | def test_write_boolean_false(): 68 | """ 69 | Checks that given the value `False` the `write_boolean` method generates 70 | the expected XML text. 71 | """ 72 | io_buffer = make_buffer() 73 | xml_writer = XmlWriter(io_buffer) 74 | Writer.write_boolean(xml_writer, 'value', False) 75 | xml_writer.flush() 76 | assert decode_buffer(io_buffer) == 'false' 77 | 78 | 79 | def test_write_integer_0(): 80 | """ 81 | Checks that given the value `0` the `write_integer` method generates 82 | the expected XML text. 83 | """ 84 | io_buffer = make_buffer() 85 | xml_writer = XmlWriter(io_buffer) 86 | Writer.write_integer(xml_writer, 'value', 0) 87 | xml_writer.flush() 88 | assert decode_buffer(io_buffer) == '0' 89 | 90 | 91 | def test_write_integer_1(): 92 | """ 93 | Checks that given the value `0` the `write_integer` method generates 94 | the expected XML text. 95 | """ 96 | io_buffer = make_buffer() 97 | xml_writer = XmlWriter(io_buffer) 98 | Writer.write_integer(xml_writer, 'value', 1) 99 | xml_writer.flush() 100 | assert decode_buffer(io_buffer) == '1' 101 | 102 | 103 | def test_write_does_not_require_xml_writer(): 104 | """ 105 | Checks that the generic `write` method doesn't require an XML writer 106 | paramater. 107 | """ 108 | vm = types.Vm() 109 | result = Writer.write(vm) 110 | assert result == '' 111 | 112 | 113 | def test_write_uses_alternative_root_tag(): 114 | """ 115 | Checks that the generic `write` method uses the alternative root tag 116 | if provided. 117 | """ 118 | vm = types.Vm() 119 | result = Writer.write(vm, root='list') 120 | assert result == '' 121 | 122 | 123 | def test_write_accepts_xml_writer(): 124 | """ 125 | Checks that the generic `write` method accepts an XML writer as 126 | parameter. 127 | """ 128 | vm = types.Vm() 129 | writer = XmlWriter(None) 130 | result = Writer.write(vm, target=writer) 131 | text = writer.string() 132 | writer.close() 133 | assert result is None 134 | assert text == '' 135 | 136 | 137 | def test_write_raises_exception_if_given_list_and_no_root(): 138 | """ 139 | Checks that the generic `write` method raises an exception if it is 140 | given a list and no root tag. 141 | """ 142 | with pytest.raises(Error) as context: 143 | Writer.write([]) 144 | assert re.search("root.*mandatory", str(context.value)) 145 | 146 | 147 | def test_write_accepts_empty_lists(): 148 | """ 149 | Checks that the generic `write` method accepts empty lists. 150 | """ 151 | result = Writer.write([], root='list') 152 | assert result == '' 153 | 154 | 155 | def test_write_accepts_list_with_one_element(): 156 | """ 157 | Checks that the generic `write` method accets lists with one 158 | element. 159 | """ 160 | vm = types.Vm() 161 | result = Writer.write([vm], root='list') 162 | assert result == '' 163 | 164 | 165 | def test_write_accepts_list_with_two_elements(): 166 | """ 167 | Checks that the generic `write` method accets lists with two 168 | elements. 169 | """ 170 | vm = types.Vm() 171 | result = Writer.write([vm, vm], root='list') 172 | assert result == '' 173 | 174 | 175 | def test_write_accepts_elements_of_different_types(): 176 | """ 177 | Checks that the generic `write` method accets lists containing 178 | elements of different types. 179 | """ 180 | vm = types.Vm() 181 | disk = types.Disk() 182 | result = Writer.write([vm, disk], root='list') 183 | assert result == '' 184 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | 23 | 4.0.0 24 | 25 | org.ovirt.engine.api 26 | python-sdk-parent 27 | pom 28 | 4.5.2-SNAPSHOT 29 | 30 | oVirt Python SDK Parent 31 | 32 | 33 | Python SDK for the oVirt Engine API. 34 | 35 | 36 | http://www.ovirt.org 37 | 38 | 39 | 40 | The Apache Software License, Version 2.0 41 | http://www.apache.org/licenses/LICENSE-2.0.txt 42 | repo 43 | 44 | 45 | 46 | 47 | 48 | oVirt Developers 49 | devel@ovirt.org 50 | oVirt 51 | http://www.ovirt.org 52 | 53 | 54 | 55 | 56 | scm:git:git@github.com:oVirt/ovirt-engine-sdk.git 57 | scm:git:git@github.com:oVirt/ovirt-engine-sdk.git 58 | https://github.com/oVirt/ovirt-engine-sdk.git 59 | HEAD 60 | 61 | 62 | 63 | 64 | 65 | UTF-8 66 | 67 | 68 | 1.3.11 69 | 4.6.1 70 | 71 | 72 | python3 73 | 74 | 75 | 76 | 77 | generator 78 | sdk 79 | 80 | 81 | 82 | 83 | Central Portal Snapshots 84 | central-portal-snapshots 85 | https://central.sonatype.com/repository/maven-snapshots/ 86 | 87 | false 88 | 89 | 90 | true 91 | 92 | 93 | 94 | 95 | 96 | 97 | javax.inject 98 | javax.inject 99 | 1 100 | provided 101 | 102 | 103 | javax.enterprise 104 | cdi-api 105 | 2.0 106 | provided 107 | 108 | 109 | javax.annotation 110 | javax.annotation-api 111 | 1.3.2 112 | provided 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | org.apache.maven.plugins 125 | maven-compiler-plugin 126 | 3.8.0 127 | 128 | 11 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-javadoc-plugin 140 | 3.2.0 141 | 142 | 11 143 | 144 | 145 | 146 | attach-javadocs 147 | 148 | jar 149 | 150 | 151 | 152 | 153 | 154 | 155 | org.apache.maven.plugins 156 | maven-source-plugin 157 | 2.4 158 | 159 | 160 | attach-sources 161 | 162 | jar-no-fork 163 | 164 | 165 | 166 | 167 | 168 | 169 | org.apache.maven.plugins 170 | maven-release-plugin 171 | 3.1.1 172 | 173 | @{version} 174 | false 175 | 176 | 177 | 178 | 179 | org.sonatype.central 180 | central-publishing-maven-plugin 181 | 0.9.0 182 | true 183 | 184 | central 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | sign 197 | 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-gpg-plugin 202 | 1.6 203 | 204 | 205 | sign-artifacts 206 | verify 207 | 208 | sign 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /sdk/lib/ovirtsdk4/writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import datetime 20 | 21 | from ovirtsdk4 import Error 22 | from ovirtsdk4 import xml 23 | 24 | 25 | class Writer(object): 26 | """ 27 | This is the base class for all the writers of the SDK. It contains 28 | the utility methods used by all of them. 29 | """ 30 | 31 | def __init__(self): 32 | pass 33 | 34 | # This dictionary stores for each known type a reference to the 35 | # method that writes the XML document corresponding for that type. 36 | # For example, for the `Vm` type it will contain a reference to the 37 | # `VmWriter.write_one` method. 38 | _writers = {} 39 | 40 | @staticmethod 41 | def write_string(writer, name, value): 42 | """ 43 | Writes an element with the given name and string value. 44 | """ 45 | return writer.write_element(name, value) 46 | 47 | @staticmethod 48 | def render_boolean(value): 49 | """ 50 | Converts the given boolean value to a string. 51 | """ 52 | if not isinstance(value, bool): 53 | raise TypeError('The \'value\' parameter must be a boolean') 54 | return 'true' if value else 'false' 55 | 56 | @staticmethod 57 | def write_boolean(writer, name, value): 58 | """ 59 | Writes an element with the given name and boolean value. 60 | """ 61 | return writer.write_element(name, Writer.render_boolean(value)) 62 | 63 | @staticmethod 64 | def render_integer(value): 65 | """ 66 | Converts the given integer value to a string. 67 | """ 68 | if not isinstance(value, int): 69 | raise TypeError('The \'value\' parameter must be an integer') 70 | return str(value) 71 | 72 | @staticmethod 73 | def write_integer(writer, name, value): 74 | """ 75 | Writes an element with the given name and integer value. 76 | """ 77 | return writer.write_element(name, Writer.render_integer(value)) 78 | 79 | @staticmethod 80 | def render_decimal(value): 81 | """ 82 | Converts the given decimal value to a string. 83 | """ 84 | if not isinstance(value, float): 85 | raise TypeError('The \'value\' parameter must be a decimal') 86 | return str(value) 87 | 88 | @staticmethod 89 | def write_decimal(writer, name, value): 90 | """ 91 | Writes an element with the given name and decimal value. 92 | """ 93 | return writer.write_element(name, Writer.render_decimal(value)) 94 | 95 | @staticmethod 96 | def render_date(value): 97 | """ 98 | Converts the given date value to a string. 99 | """ 100 | if not isinstance(value, datetime.datetime): 101 | raise TypeError('The \'value\' parameter must be a date') 102 | return value.isoformat() 103 | 104 | @staticmethod 105 | def write_date(writer, name, value): 106 | """ 107 | Writes an element with the given name and date value. 108 | """ 109 | return writer.write_element(name, Writer.render_date(value)) 110 | 111 | @classmethod 112 | def register(cls, typ, writer): 113 | """ 114 | Registers a write method. 115 | 116 | :param typ: The type. 117 | 118 | :param writer: The reference to the method that writes the XML 119 | object corresponding to the type. 120 | """ 121 | cls._writers[typ] = writer 122 | 123 | @classmethod 124 | def write(cls, obj, target=None, root=None, indent=False): 125 | """ 126 | Writes one object, determining the writer method to use based on 127 | the type. For example, if the type of the object is `Vm` then it 128 | will write the `vm` tag, with its contents. 129 | 130 | :param obj: The object to write. 131 | 132 | :param target: The XML writer where the output will be written. If 133 | this parameter isn't given, or if the value is `None` the method 134 | will return a string containing the XML document. 135 | 136 | :param root: The name of the root tag of the generated XML document. 137 | This isn't needed when writing single objects, as the tag is 138 | calculated from the type of the object. For example, if the 139 | object isa virtual machine then the tag will be `vm`. But when 140 | writing lists of objects the it is needed, because the list may 141 | be empty, or have different types of objects. In this case, for 142 | lists, if it isn't provided an exception will be raised. 143 | 144 | :param indent: Indicates if the output should be indented, for 145 | easier reading by humans. 146 | """ 147 | # If the target is `None` then create a temporary XML writer to 148 | # write the output: 149 | cursor = None 150 | if target is None: 151 | cursor = xml.XmlWriter(None, indent) 152 | elif isinstance(target, xml.XmlWriter): 153 | cursor = target 154 | else: 155 | raise Error( 156 | 'Expected an \'XmlWriter\', but got \'%s\'' % 157 | type(target) 158 | ) 159 | 160 | # Do the actual write, and make sure to always close the XML 161 | # writer if we created it: 162 | try: 163 | if isinstance(obj, list): 164 | # For lists we can't decide which tag to use, so the 165 | # 'root' parameter is mandatory in this case: 166 | if root is None: 167 | raise Error( 168 | 'The \'root\' parameter is mandatory when ' 169 | 'writing lists.' 170 | ) 171 | 172 | # Write the root tag, and then recursively call the 173 | # method to write each of the items of the list: 174 | cursor.write_start(root) 175 | for item in obj: 176 | cls.write(item, target=cursor) 177 | cursor.write_end() 178 | else: 179 | # Select the specific writer according to the type: 180 | typ = type(obj) 181 | writer = cls._writers.get(typ) 182 | if writer is None: 183 | raise Error( 184 | 'Can\'t find a writer for type \'%s\'' % 185 | typ 186 | ) 187 | 188 | # Write the object using the specific method: 189 | writer(obj, cursor, root) 190 | 191 | # If no XML cursor was explicitly given, and we created it, 192 | # then we need to return the generated XML text: 193 | if target is None: 194 | return cursor.string() 195 | finally: 196 | if cursor is not None and cursor != target: 197 | cursor.close() 198 | -------------------------------------------------------------------------------- /sdk/tests/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import json 20 | import os.path 21 | import ovirtsdk4 as sdk 22 | import re 23 | import socket 24 | import ssl 25 | 26 | try: 27 | from http.server import HTTPServer 28 | from http.server import SimpleHTTPRequestHandler 29 | except ImportError: 30 | from BaseHTTPServer import HTTPServer 31 | from SimpleHTTPServer import SimpleHTTPRequestHandler 32 | 33 | from threading import Thread 34 | from time import sleep 35 | 36 | try: 37 | from urllib.parse import urlparse 38 | except ImportError: 39 | from urlparse import urlparse 40 | 41 | 42 | class TestHandler(SimpleHTTPRequestHandler): 43 | # Path handlers: 44 | handlers = {} 45 | 46 | def log_message(self, format, *args): 47 | """ 48 | Empty method, so we don't mix output of HTTP server with tests 49 | """ 50 | pass 51 | 52 | def do_GET(self): 53 | params = urlparse(self.path) 54 | 55 | if params.path in self.handlers: 56 | self.handlers[params.path](self) 57 | else: 58 | SimpleHTTPRequestHandler.do_GET(self) 59 | 60 | def do_POST(self): 61 | params = urlparse(self.path) 62 | 63 | if params.path in self.handlers: 64 | self.handlers[params.path](self) 65 | else: 66 | SimpleHTTPRequestHandler.do_POST(self) 67 | 68 | 69 | class TestServer(object): 70 | # The authentication details used by the embedded tests web server: 71 | REALM = 'API' 72 | USER = 'admin@internal' 73 | PASSWORD = 'vzhJgfyaDPHRhg' 74 | TOKEN = 'bvY7txV9ltmmRQ' 75 | 76 | # The host and port and path used by the embedded tests web server: 77 | HOST = 'localhost' 78 | PREFIX = '/ovirt-engine' 79 | PORT = None 80 | 81 | # The embedded web server: 82 | _httpd = None 83 | # Thread for http server: 84 | _thread = None 85 | 86 | def _get_request_content(self, handler): 87 | content_len = int(handler.headers.get('content-length', 0)) 88 | content = handler.rfile.read(content_len).decode('utf-8') 89 | content = re.sub(r">\s+<", "><", content) 90 | return content.strip() 91 | 92 | def set_xml_response(self, path, code, body, delay=0): 93 | def _handle_request(handler): 94 | # Store request query parameter: 95 | self.last_request_query = urlparse(handler.path).query 96 | # Store request content: 97 | self.last_request_content = self._get_request_content(handler) 98 | # Store request headers: 99 | self.last_request_headers = handler.headers 100 | 101 | authorization = handler.headers.get('Authorization') 102 | if authorization != "Bearer %s" % self.TOKEN: 103 | data = '401 - Unauthorized'.encode('utf-8') 104 | 105 | handler.send_response(401) 106 | handler.send_header('Content-Length', len(data)) 107 | handler.end_headers() 108 | handler.wfile.write() 109 | else: 110 | data = body.encode('utf-8') 111 | 112 | sleep(delay) 113 | handler.send_response(code) 114 | handler.send_header('Content-Type', 'application/xml') 115 | handler.send_header('Content-Length', len(data)) 116 | handler.end_headers() 117 | 118 | handler.wfile.write(data) 119 | 120 | TestHandler.handlers[ 121 | '%s/api%s' % (self.prefix(), '/%s' % path if path else '') 122 | ] = _handle_request 123 | 124 | def set_json_response(self, path, code, body): 125 | def _handle_request(handler): 126 | data = json.dumps(body, ensure_ascii=False).encode('utf-8') 127 | 128 | handler.send_response(code) 129 | handler.send_header('Content-Type', 'application/json') 130 | handler.send_header('Content-Length', len(data)) 131 | handler.end_headers() 132 | 133 | handler.wfile.write(data) 134 | 135 | TestHandler.handlers[path] = _handle_request 136 | 137 | def start_server(self, host='localhost'): 138 | self._httpd = HTTPServer((self.host(), self.port()), TestHandler) 139 | context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 140 | context.minimum_version = ssl.TLSVersion.TLSv1_2 141 | context.load_cert_chain( 142 | certfile=self.__absolute_path('%s.crt' % host), 143 | keyfile=self.__absolute_path('%s.key' % host) 144 | ) 145 | self._httpd.socket = context.wrap_socket( 146 | self._httpd.socket, 147 | server_side=True 148 | ) 149 | # Path handler for username/password authentication service: 150 | self.set_json_response( 151 | path='%s/sso/oauth/token' % self.prefix(), 152 | code=200, 153 | body={"access_token": self.TOKEN} 154 | ) 155 | # SSO Logout service: 156 | self.set_json_response( 157 | path='%s/services/sso-logout' % self.prefix(), 158 | code=200, 159 | body={"access_token": self.TOKEN} 160 | ) 161 | # Path handler for Kerberos authentication service: 162 | self.set_json_response( 163 | path='%s/sso/oauth/token-http-auth' % self.prefix(), 164 | code=200, 165 | body={"access_token": self.TOKEN} 166 | ) 167 | 168 | # Server requests in different thread, because it block current thread 169 | self._thread = Thread(target=self._httpd.serve_forever) 170 | self._thread.start() 171 | 172 | def stop_server(self): 173 | self._httpd.shutdown() 174 | self._thread.join() 175 | 176 | def port(self): 177 | if self.PORT is None: 178 | server = None 179 | for port in range(60000, 61000): 180 | try: 181 | server = HTTPServer( 182 | (self.host(), port), 183 | TestHandler 184 | ) 185 | self.PORT = port 186 | break 187 | except socket.error: 188 | pass 189 | if server is None: 190 | raise Exception("Can't find a free port") 191 | 192 | return self.PORT 193 | 194 | def prefix(self): 195 | return self.PREFIX 196 | 197 | def url(self): 198 | return "https://{host}:{port}{prefix}/api".format( 199 | host=self.host(), 200 | port=self.port(), 201 | prefix=self.prefix(), 202 | ) 203 | 204 | def ca_file(self): 205 | return self.__absolute_path('ca.crt') 206 | 207 | def user(self): 208 | return self.USER 209 | 210 | def password(self): 211 | return self.PASSWORD 212 | 213 | def host(self): 214 | return self.HOST 215 | 216 | def connection(self, headers=None): 217 | return sdk.Connection( 218 | url=self.url(), 219 | username=self.user(), 220 | password=self.password(), 221 | ca_file=self.ca_file(), 222 | headers=headers, 223 | ) 224 | 225 | def __absolute_path(self, str): 226 | return os.path.join(os.path.dirname(__file__), 'pki/%s' % str) 227 | -------------------------------------------------------------------------------- /generator/src/main/java/org/ovirt/sdk/python/TypesGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | The oVirt Project - Ovirt Engine SDK 3 | 4 | Copyright oVirt Authors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package org.ovirt.sdk.python; 20 | 21 | import static java.util.stream.Collectors.toCollection; 22 | import static java.util.stream.Collectors.toSet; 23 | 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.util.ArrayDeque; 27 | import java.util.Deque; 28 | import java.util.HashSet; 29 | import java.util.Set; 30 | import java.util.stream.Stream; 31 | import javax.inject.Inject; 32 | 33 | import org.ovirt.api.metamodel.concepts.EnumType; 34 | import org.ovirt.api.metamodel.concepts.EnumValue; 35 | import org.ovirt.api.metamodel.concepts.Model; 36 | import org.ovirt.api.metamodel.concepts.Name; 37 | import org.ovirt.api.metamodel.concepts.StructMember; 38 | import org.ovirt.api.metamodel.concepts.StructType; 39 | import org.ovirt.api.metamodel.concepts.Type; 40 | import org.ovirt.api.metamodel.tool.Names; 41 | 42 | /** 43 | * This class is responsible for generating the classes that represent the types of the model. 44 | */ 45 | public class TypesGenerator implements PythonGenerator { 46 | /** 47 | * The directory were the output will be generated: 48 | */ 49 | protected File out; 50 | 51 | // Reference to the objects used to generate the code: 52 | @Inject 53 | private Names names; 54 | @Inject 55 | private PythonNames pythonNames; 56 | 57 | // The buffer used to generate the code: 58 | private PythonBuffer buffer; 59 | 60 | public void setOut(File newOut) { 61 | out = newOut; 62 | } 63 | 64 | public void generate(Model model) { 65 | // Prepare the buffer: 66 | buffer = new PythonBuffer(); 67 | buffer.setModuleName(pythonNames.getTypesModuleName()); 68 | 69 | // Generate the code: 70 | generateTypes(model); 71 | 72 | // Write the file: 73 | try { 74 | buffer.write(out); 75 | } 76 | catch (IOException exception) { 77 | throw new RuntimeException("Error writing types module", exception); 78 | } 79 | } 80 | 81 | private void generateTypes(Model model) { 82 | // Generate the import statements for structs that aren't generated: 83 | String rootModuleName = pythonNames.getRootModuleName(); 84 | buffer.addImport("from enum import Enum, unique", rootModuleName); 85 | buffer.addImport("from %1$s import Struct", rootModuleName); 86 | 87 | // The declarations of struct types need to appear in inheritance order, otherwise some symbols won't be 88 | // defined and that will produce errors. To order them correctly we need first to sort them by name, and 89 | // then sort again so that bases are before extensions. 90 | Deque pending = model.types() 91 | .filter(StructType.class::isInstance) 92 | .map(StructType.class::cast) 93 | .sorted() 94 | .collect(toCollection(ArrayDeque::new)); 95 | Deque structs = new ArrayDeque<>(pending.size()); 96 | while (!pending.isEmpty()) { 97 | StructType current = pending.removeFirst(); 98 | StructType base = (StructType) current.getBase(); 99 | if (base == null || structs.contains(base)) { 100 | structs.addLast(current); 101 | } 102 | else { 103 | pending.addLast(current); 104 | } 105 | } 106 | structs.stream() 107 | .forEach(this::generateStruct); 108 | 109 | // Enum types don't need any special order, so we sort them only by name: 110 | model.types() 111 | .filter(EnumType.class::isInstance) 112 | .map(EnumType.class::cast) 113 | .sorted() 114 | .forEach(this::generateEnum); 115 | } 116 | 117 | private void generateStruct(StructType type) { 118 | // Begin class: 119 | PythonClassName typeName = pythonNames.getTypeName(type); 120 | Type base = type.getBase(); 121 | String baseName = base != null? pythonNames.getTypeName(base).getClassName(): "Struct"; 122 | buffer.addLine("class %1$s(%2$s):", typeName.getClassName(), baseName); 123 | buffer.startBlock(); 124 | buffer.addLine(); 125 | 126 | // Constructor with a named parameter for each attribute and link: 127 | Set allMembers = Stream.concat(type.attributes(), type.links()) 128 | .collect(toSet()); 129 | Set declaredMembers = Stream.concat(type.declaredAttributes(), type.declaredLinks()) 130 | .collect(toSet()); 131 | Set inheritedMembers = new HashSet<>(allMembers); 132 | inheritedMembers.removeAll(declaredMembers); 133 | buffer.addLine("def __init__("); 134 | buffer.startBlock(); 135 | buffer.addLine("self,"); 136 | allMembers.stream().sorted().forEach(this::generateMemberFormalParameter); 137 | buffer.endBlock(); 138 | buffer.addLine("):"); 139 | buffer.startBlock(); 140 | buffer.addLine("super(%1$s, self).__init__(", typeName.getClassName()); 141 | buffer.startBlock(); 142 | inheritedMembers.stream().sorted().forEach(this::generateMemberPropagate); 143 | buffer.endBlock(); 144 | buffer.addLine(")"); 145 | if (!declaredMembers.isEmpty()) { 146 | declaredMembers.stream().sorted().forEach(this::generateMemberInitialization); 147 | } 148 | else { 149 | buffer.addLine("pass"); 150 | } 151 | buffer.endBlock(); 152 | buffer.addLine(); 153 | 154 | // Generate getters and setters for attributes and links: 155 | declaredMembers.forEach(this::generateMember); 156 | 157 | // End class: 158 | buffer.endBlock(); 159 | buffer.addLine(); 160 | } 161 | 162 | private void generateMember(StructMember member) { 163 | generateGetter(member); 164 | generateSetter(member); 165 | } 166 | 167 | private void generateGetter(StructMember member) { 168 | Name name = member.getName(); 169 | String property = pythonNames.getMemberStyleName(name); 170 | buffer.addLine("@property"); 171 | buffer.addLine("def %1$s(self):", property); 172 | buffer.startBlock(); 173 | buffer.startComment(); 174 | buffer.addLine("Returns the value of the `%1$s` property.", property); 175 | buffer.endComment(); 176 | buffer.addLine("return self._%1$s", property); 177 | buffer.endBlock(); 178 | buffer.addLine(); 179 | } 180 | 181 | private void generateSetter(StructMember member) { 182 | Name name = member.getName(); 183 | String property = pythonNames.getMemberStyleName(name); 184 | buffer.addLine("@%1$s.setter", property); 185 | buffer.addLine("def %1$s(self, value):", property); 186 | buffer.startBlock(); 187 | buffer.startComment(); 188 | buffer.addLine("Sets the value of the `%1$s` property.", property); 189 | buffer.endComment(); 190 | generateCheckType(member, "value"); 191 | buffer.addLine("self._%1$s = value", property); 192 | buffer.endBlock(); 193 | buffer.addLine(); 194 | } 195 | 196 | private void generateCheckType(StructMember member, String value) { 197 | Type type = member.getType(); 198 | if (type instanceof StructType || type instanceof EnumType) { 199 | Name name = member.getName(); 200 | String property = pythonNames.getMemberStyleName(name); 201 | PythonClassName typeName = pythonNames.getTypeName(type); 202 | buffer.addLine("Struct._check_type('%1$s', %2$s, %3$s)", property, value, typeName.getClassName()); 203 | } 204 | } 205 | 206 | private void generateEnum(EnumType type) { 207 | // Begin class: 208 | PythonClassName typeName = pythonNames.getTypeName(type); 209 | buffer.addLine("@unique"); 210 | buffer.addLine("class %1$s(Enum):", typeName.getClassName()); 211 | 212 | // Begin body: 213 | buffer.startBlock(); 214 | 215 | // Values: 216 | type.values().sorted().forEach(this::generateEnumValue); 217 | buffer.addLine(); 218 | 219 | // Constructor: 220 | buffer.addLine("def __init__(self, image):"); 221 | buffer.startBlock(); 222 | buffer.addLine("self._image = image"); 223 | buffer.endBlock(); 224 | buffer.addLine(); 225 | 226 | // Method to convert to string: 227 | buffer.addLine("def __str__(self):"); 228 | buffer.startBlock(); 229 | buffer.addLine("return self._image"); 230 | buffer.endBlock(); 231 | buffer.addLine(); 232 | 233 | // End body: 234 | buffer.endBlock(); 235 | buffer.addLine(); 236 | } 237 | 238 | private void generateEnumValue(EnumValue value) { 239 | Name name = value.getName(); 240 | String constantName = pythonNames.getConstantStyleName(name); 241 | String constantValue = names.getLowerJoined(name, "_"); 242 | buffer.addLine("%1$s = '%2$s'", constantName, constantValue); 243 | } 244 | 245 | private void generateMemberFormalParameter(StructMember member) { 246 | buffer.addLine("%1$s=None,", pythonNames.getMemberStyleName(member.getName())); 247 | } 248 | 249 | private void generateMemberInitialization(StructMember member) { 250 | buffer.addLine("self.%1$s = %1$s", pythonNames.getMemberStyleName(member.getName())); 251 | } 252 | 253 | private void generateMemberPropagate(StructMember member) { 254 | buffer.addLine("%1$s=%1$s,", pythonNames.getMemberStyleName(member.getName())); 255 | } 256 | } 257 | 258 | -------------------------------------------------------------------------------- /sdk/LICENSE.txt: -------------------------------------------------------------------------------- 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 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /generator/src/main/java/org/ovirt/sdk/python/PythonNames.java: -------------------------------------------------------------------------------- 1 | /* 2 | The oVirt Project - Ovirt Engine SDK 3 | 4 | Copyright oVirt Authors 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package org.ovirt.sdk.python; 20 | 21 | import static java.util.stream.Collectors.joining; 22 | 23 | import java.util.List; 24 | import java.util.Set; 25 | import javax.enterprise.context.ApplicationScoped; 26 | import javax.inject.Inject; 27 | 28 | import org.ovirt.api.metamodel.concepts.EnumType; 29 | import org.ovirt.api.metamodel.concepts.ListType; 30 | import org.ovirt.api.metamodel.concepts.Model; 31 | import org.ovirt.api.metamodel.concepts.Name; 32 | import org.ovirt.api.metamodel.concepts.NameParser; 33 | import org.ovirt.api.metamodel.concepts.PrimitiveType; 34 | import org.ovirt.api.metamodel.concepts.Service; 35 | import org.ovirt.api.metamodel.concepts.StructType; 36 | import org.ovirt.api.metamodel.concepts.Type; 37 | import org.ovirt.api.metamodel.tool.ReservedWords; 38 | import org.ovirt.api.metamodel.tool.Words; 39 | 40 | /** 41 | * This class contains the rules used to calculate the names of generated Python concepts. 42 | */ 43 | @ApplicationScoped 44 | public class PythonNames { 45 | // The names of the base classes: 46 | public static final Name READER_NAME = NameParser.parseUsingCase("Reader"); 47 | public static final Name SERVICE_NAME = NameParser.parseUsingCase("Service"); 48 | public static final Name WRITER_NAME = NameParser.parseUsingCase("Writer"); 49 | 50 | // The relative names of the modules: 51 | public static final String READERS_MODULE = "readers"; 52 | public static final String SERVICES_MODULE = "services"; 53 | public static final String TYPES_MODULE = "types"; 54 | public static final String WRITERS_MODULE = "writers"; 55 | public static final String VERSION_MODULE = "version"; 56 | 57 | // Reference to the object used to do computations with words. 58 | @Inject 59 | private Words words; 60 | 61 | // We need the Python reserved words in order to avoid producing names that aren't legal: 62 | @Inject 63 | @ReservedWords(language = "python") 64 | private Set reservedWords; 65 | 66 | // The name of the root module: 67 | private String rootModuleName = "ovirtsdk4"; 68 | 69 | // The version number: 70 | private String version; 71 | 72 | /** 73 | * Sets the version. 74 | * @param newVersion the new version in string form. 75 | */ 76 | public void setVersion(String newVersion) { 77 | version = newVersion; 78 | } 79 | 80 | /** 81 | * Get the version. 82 | * @return the version in string form. 83 | */ 84 | public String getVersion() { 85 | return version; 86 | } 87 | 88 | /** 89 | * Get the name of the root module. 90 | * @return the name of the root module. 91 | */ 92 | public String getRootModuleName() { 93 | return rootModuleName; 94 | } 95 | 96 | /** 97 | * Get the name of the types module. 98 | * @return the name of the types module. 99 | */ 100 | public String getTypesModuleName() { 101 | return getModuleName(TYPES_MODULE); 102 | } 103 | 104 | /** 105 | * Get the name of the readers module. 106 | * @return the name of the readers module. 107 | */ 108 | public String getReadersModuleName() { 109 | return getModuleName(READERS_MODULE); 110 | } 111 | 112 | /** 113 | * Get the name of the writers module. 114 | * @return the name of the writers module. 115 | */ 116 | public String getWritersModuleName() { 117 | return getModuleName(WRITERS_MODULE); 118 | } 119 | 120 | /** 121 | * Get the name of the services module. 122 | * @return the name of the services module. 123 | */ 124 | public String getServicesModuleName() { 125 | return getModuleName(SERVICES_MODULE); 126 | } 127 | 128 | /** 129 | * Get the name of the version module. 130 | * @return the name of the version module. 131 | */ 132 | public String getVersionModuleName() { 133 | return getModuleName(VERSION_MODULE); 134 | } 135 | 136 | /** 137 | * Get the complete name of the given module. 138 | * @param relativeNames the relative names of the module, if any. 139 | * @return the complete name of the module. 140 | */ 141 | public String getModuleName(String... relativeNames) { 142 | StringBuilder buffer = new StringBuilder(); 143 | buffer.append(rootModuleName); 144 | if (relativeNames != null || relativeNames.length > 0) { 145 | for (String relativeName : relativeNames) { 146 | buffer.append('.'); 147 | buffer.append(relativeName); 148 | } 149 | } 150 | return buffer.toString(); 151 | } 152 | 153 | /** 154 | * Calculates the Python name that corresponds to the given type. 155 | * @param type the type for which to build the class name. 156 | * @return the Python class name. 157 | */ 158 | public PythonClassName getTypeName(Type type) { 159 | return buildClassName(type.getName(), null, TYPES_MODULE); 160 | } 161 | 162 | /** 163 | * Calculates that should be used in Python to reference the given type. 164 | * @param type the type for which to build the reference. 165 | * @return the python reference. For example, for the boolean type it will the {@code bool} string. 166 | */ 167 | public PythonTypeReference getTypeReference(Type type) { 168 | PythonTypeReference reference = new PythonTypeReference(); 169 | if (type instanceof PrimitiveType) { 170 | Model model = type.getModel(); 171 | if (type == model.getBooleanType()) { 172 | reference.setText("bool"); 173 | } 174 | else if (type == model.getIntegerType()) { 175 | reference.setText("int"); 176 | } 177 | else if (type == model.getDecimalType()) { 178 | reference.setText("float"); 179 | } 180 | else if (type == model.getStringType()) { 181 | reference.setText("str"); 182 | } 183 | else if (type == model.getDateType()) { 184 | reference.addImport("import datatime"); 185 | reference.setText("datetime.date"); 186 | } 187 | else { 188 | throw new IllegalArgumentException( 189 | "Don't know how to build reference for primitive type \"" + type + "\"" 190 | ); 191 | } 192 | } 193 | else if (type instanceof StructType || type instanceof EnumType) { 194 | reference.addImport(String.format("from %1$s import %2$s", getRootModuleName(), TYPES_MODULE)); 195 | reference.setText(TYPES_MODULE + "." + getTypeName(type).getClassName()); 196 | } 197 | else if (type instanceof ListType) { 198 | reference.setText("list"); 199 | } 200 | else { 201 | throw new IllegalArgumentException("Don't know how to build reference for type \"" + type + "\""); 202 | } 203 | return reference; 204 | } 205 | /** 206 | * Calculates the Python name of the base class of the services. 207 | * @return the python class name object of the base service. 208 | */ 209 | public PythonClassName getBaseServiceName() { 210 | return buildClassName(SERVICE_NAME, null, SERVICES_MODULE); 211 | } 212 | 213 | /** 214 | * Calculates the Python name that corresponds to the given service. 215 | * @param service the service for which to build the class name. 216 | * @return the Python class name object. 217 | */ 218 | public PythonClassName getServiceName(Service service) { 219 | return buildClassName(service.getName(), SERVICE_NAME, SERVICES_MODULE); 220 | } 221 | 222 | /** 223 | * Calculates the Python name of the reader for the given type. 224 | * @param type the type for which to build the class name. 225 | * @return the Python class name object. 226 | */ 227 | public PythonClassName getReaderName(Type type) { 228 | return buildClassName(type.getName(), READER_NAME, READERS_MODULE); 229 | } 230 | 231 | /** 232 | * Calculates the Python name of the writer for the given type. 233 | * @param type the type for which to build the class name. 234 | * @return the Python class name object. 235 | */ 236 | public PythonClassName getWriterName(Type type) { 237 | return buildClassName(type.getName(), WRITER_NAME, WRITERS_MODULE); 238 | } 239 | 240 | /** 241 | * Builds a Python name from the given base name, suffix, and module. 242 | * 243 | * The suffix can be {@code null} or empty, in that case then won't be added. 244 | * 245 | * @param base the base name 246 | * @param suffix the suffix to add to the name 247 | * @param module the module name 248 | * @return the calculated Python class name 249 | */ 250 | private PythonClassName buildClassName(Name base, Name suffix, String module) { 251 | List words = base.getWords(); 252 | if (suffix != null) { 253 | words.addAll(suffix.getWords()); 254 | } 255 | Name name = new Name(words); 256 | PythonClassName result = new PythonClassName(); 257 | result.setClassName(getClassStyleName(name)); 258 | result.setModuleName(getModuleName(module)); 259 | return result; 260 | } 261 | 262 | /** 263 | * Returns a representation of the given name using the capitalization style typically used for Python classes. 264 | * @param name the given name. 265 | * @return a string with the name in class style. 266 | */ 267 | public String getClassStyleName(Name name) { 268 | return name.words().map(words::capitalize).collect(joining()); 269 | } 270 | 271 | /** 272 | * Returns a representation of the given name using the capitalization style typically used for Python members. 273 | * @param name the given name. 274 | * @return a string with the name in member style. 275 | */ 276 | public String getMemberStyleName(Name name) { 277 | String result = name.words().map(String::toLowerCase).collect(joining("_")); 278 | if (reservedWords.contains(result)) { 279 | result += "_"; 280 | } 281 | return result; 282 | } 283 | 284 | /** 285 | * Returns a representation of the given name using the capitalization style typically used for Python constants. 286 | * @param name the given name 287 | * @return a string with the name in constant style. 288 | */ 289 | public String getConstantStyleName(Name name) { 290 | return name.words().map(String::toUpperCase).collect(joining("_")); 291 | } 292 | 293 | /** 294 | * Returns a representation of the given name using the capitalization style typically used for Python modules. 295 | * @param name the given name 296 | * @return a string with the name in module style. 297 | */ 298 | public String getModuleStyleName(Name name) { 299 | String result = name.words().map(String::toLowerCase).collect(joining("_")); 300 | if (reservedWords.contains(result)) { 301 | result += "_"; 302 | } 303 | return result; 304 | } 305 | } 306 | 307 | -------------------------------------------------------------------------------- /sdk/lib/ovirtsdk4/reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import datetime 20 | import time 21 | import io 22 | import re 23 | 24 | from ovirtsdk4 import Error 25 | from ovirtsdk4 import xml 26 | 27 | 28 | # Regular expression used to check if the representation of a date contains a 29 | # time zone offset: 30 | TZ_OFFSET_RE = re.compile( 31 | r'(?P[+-])(?P\d{2}):(?P\d{2}$)' 32 | ) 33 | 34 | # Regular expression used to check if the representation of the date contains 35 | # the number of microseconds: 36 | TZ_USEC_RE = re.compile( 37 | r'\.\d+$' 38 | ) 39 | 40 | 41 | class TZ(datetime.tzinfo): 42 | """ 43 | This is a simple implementation of the `tzinfo` class, that contains a 44 | fixed offset. 45 | """ 46 | 47 | def __init__(self, minutes, name): 48 | super(TZ, self).__init__() 49 | self._delta = datetime.timedelta(minutes=minutes) 50 | self._name = name 51 | 52 | def dst(self, date_time): 53 | return None 54 | 55 | def tzname(self, date_time): 56 | return self._name 57 | 58 | def utcoffset(self, date_time): 59 | return self._delta 60 | 61 | 62 | class Reader(object): 63 | """ 64 | This is the base class for all the readers of the SDK. It contains 65 | the utility methods used by all of them. 66 | """ 67 | 68 | # This dictionary stores for each known tag a reference to the method 69 | # that read the object corresponding for that tag. For example, for the 70 | # `vm` tag it will contain a reference to the `VmReader.read_one` method, 71 | # and for the `vms` tag it will contain a reference to the 72 | # `VmReader.read_many` method. 73 | _readers = {} 74 | 75 | def __init__(self): 76 | pass 77 | 78 | @staticmethod 79 | def read_string(reader): 80 | """ 81 | Reads a string value, assuming that the cursor is positioned at the 82 | start element that contains the value. 83 | """ 84 | return reader.read_element() 85 | 86 | @staticmethod 87 | def read_strings(reader): 88 | """ 89 | Reads a list of string values, assuming that the cursor is positioned 90 | at the start element of the element that contains the first value. 91 | """ 92 | return reader.read_elements() 93 | 94 | @staticmethod 95 | def parse_boolean(text): 96 | """ 97 | Converts the given text to a boolean value. 98 | """ 99 | if text is None: 100 | return None 101 | text = text.lower() 102 | if text == 'false' or text == '0': 103 | return False 104 | if text == 'true' or text == '1': 105 | return True 106 | raise ValueError('The text \'%s\' isn\'t a valid boolean value' % text) 107 | 108 | @staticmethod 109 | def read_boolean(reader): 110 | """ 111 | Reads a boolean value, assuming that the cursor is positioned at the 112 | start element that contains the value. 113 | """ 114 | return Reader.parse_boolean(reader.read_element()) 115 | 116 | @staticmethod 117 | def read_booleans(reader): 118 | """ 119 | Reads a list of boolean values, assuming that the cursor is positioned 120 | at the start element of the element that contains the first value. 121 | """ 122 | return list(map(Reader.parse_boolean, reader.read_elements())) 123 | 124 | @staticmethod 125 | def parse_integer(text): 126 | """ 127 | Converts the given text to an integer value. 128 | """ 129 | if text is None: 130 | return None 131 | try: 132 | return int(text) 133 | except ValueError: 134 | raise ValueError( 135 | 'The text \'%s\' isn\'t a valid integer value' % text 136 | ) 137 | 138 | @staticmethod 139 | def read_integer(reader): 140 | """ 141 | Reads an integer value, assuming that the cursor is positioned at the 142 | start element that contains the value. 143 | """ 144 | return Reader.parse_integer(reader.read_element()) 145 | 146 | @staticmethod 147 | def read_integers(reader): 148 | """ 149 | Reads a list of integer values, assuming that the cursor is positioned 150 | at the start element of the element that contains the first value. 151 | """ 152 | return list(map(Reader.parse_integer, reader.read_elements())) 153 | 154 | @staticmethod 155 | def parse_decimal(text): 156 | """ 157 | Converts the given text to a decimal value. 158 | """ 159 | if text is None: 160 | return None 161 | try: 162 | return float(text) 163 | except ValueError: 164 | raise ValueError( 165 | 'The text \'%s\' isn\'t a valid decimal value' % text 166 | ) 167 | 168 | @staticmethod 169 | def read_decimal(reader): 170 | """ 171 | Reads a decimal value, assuming that the cursor is positioned at the 172 | start element that contains the value. 173 | """ 174 | return Reader.parse_decimal(reader.read_element()) 175 | 176 | @staticmethod 177 | def read_decimals(reader): 178 | """ 179 | Reads a list of decimal values, assuming that the cursor is positioned 180 | at the start element of the element that contains the first value. 181 | """ 182 | return list(map(Reader.parse_decimal, reader.read_elements())) 183 | 184 | @staticmethod 185 | def parse_date(text): 186 | """ 187 | Converts the given text to a date value. 188 | """ 189 | 190 | # Check the text has a value: 191 | if text is None: 192 | return None 193 | 194 | # Extract the time zone: 195 | tz = None 196 | if text[-1] == 'Z': 197 | tz = TZ(0, 'UTC') 198 | text = text[:-1] 199 | else: 200 | match = TZ_OFFSET_RE.search(text) 201 | if match: 202 | name = match.group(0) 203 | sign = match.group('sign') 204 | hours = int(match.group('hours')) 205 | minutes = int(match.group('minutes')) 206 | offset = hours * 60 + minutes 207 | if sign == '-': 208 | offset *= -1 209 | tz = TZ(offset, name) 210 | text = text[:-len(name)] 211 | 212 | # Parse the rest of the date: 213 | format = '%Y-%m-%dT%H:%M:%S' 214 | if TZ_USEC_RE.search(text): 215 | format += '.%f' 216 | try: 217 | try: 218 | date = datetime.datetime.strptime(text, format) 219 | except TypeError: 220 | # when have TypeError: attribute of type 'NoneType' 221 | # workaround to treat a issue of python module: https://bugs.python.org/issue27400 222 | date = datetime.datetime.fromtimestamp(time.mktime(time.strptime(text, format))) 223 | except ValueError: 224 | raise ValueError( 225 | 'The text \'%s\' isn\'t a valid date value' % text 226 | ) 227 | 228 | # Set the time zone: 229 | if tz is not None: 230 | date = date.replace(tzinfo=tz) 231 | 232 | return date 233 | 234 | @staticmethod 235 | def read_date(reader): 236 | """ 237 | Reads a date value, assuming that the cursor is positioned at the 238 | start element that contains the value. 239 | """ 240 | return Reader.parse_date(reader.read_element()) 241 | 242 | @staticmethod 243 | def read_dates(reader): 244 | """ 245 | Reads a list of date values, assuming that the cursor is positioned 246 | at the start element of the element that contains the first value. 247 | """ 248 | return list(map(Reader.parse_date, reader.read_elements())) 249 | 250 | @staticmethod 251 | def parse_enum(enum_type, text): 252 | """ 253 | Converts the given text to an enum. 254 | """ 255 | if text is None: 256 | return None 257 | try: 258 | return enum_type(text) 259 | except ValueError: 260 | return None 261 | 262 | @staticmethod 263 | def read_enum(enum_type, reader): 264 | """ 265 | Reads a enum value, assuming that the cursor is positioned at the 266 | start element that contains the value. 267 | """ 268 | return Reader.parse_enum(enum_type, reader.read_element()) 269 | 270 | @staticmethod 271 | def read_enums(enum_type, reader): 272 | """ 273 | Reads a list of enum values, assuming that the cursor is positioned 274 | at the start element of the element that contains the first value. 275 | """ 276 | return [ 277 | Reader.parse_enum(enum_type, e) for e in reader.read_elements() 278 | ] 279 | 280 | @classmethod 281 | def register(cls, tag, reader): 282 | """ 283 | Registers a read method. 284 | 285 | :param tag: The tag name. 286 | :param reader: The reference to the method that reads the object corresponding to the `tag`. 287 | """ 288 | cls._readers[tag] = reader 289 | 290 | @classmethod 291 | def read(cls, source): 292 | """ 293 | Reads one object, determining the reader method to use based on the 294 | tag name of the first element. For example, if the first tag name 295 | is `vm` then it will create a `Vm` object, if it the tag is `vms` 296 | it will create an array of `Vm` objects, so on. 297 | 298 | :param source: The string, IO or XML reader where the input will be taken from. 299 | """ 300 | # If the source is a string or IO object then create a XML reader from it: 301 | cursor = None 302 | if isinstance(source, str): 303 | # In Python 3 str is a list of 16 bits characters, so it 304 | # needs to be converted to an array of bytes, using UTF-8, 305 | # before trying to parse it. 306 | source = source.encode('utf-8') 307 | cursor = xml.XmlReader(io.BytesIO(source)) 308 | elif isinstance(source, bytes): 309 | cursor = xml.XmlReader(io.BytesIO(source)) 310 | elif isinstance(source, io.BytesIO): 311 | cursor = xml.XmlReader(source) 312 | elif isinstance(source, xml.XmlReader): 313 | cursor = source 314 | else: 315 | raise AttributeError( 316 | "Expected a 'str', 'BytesIO' or 'XmlReader', but got '{source}'".format( 317 | source=type(source) 318 | ) 319 | ) 320 | 321 | try: 322 | # Do nothing if there aren't more tags: 323 | if not cursor.forward(): 324 | return None 325 | 326 | # Select the specific reader according to the tag: 327 | tag = cursor.node_name() 328 | reader = cls._readers.get(tag) 329 | if reader is None: 330 | raise Error( 331 | "Can't find a reader for tag '{tag}'".format(tag=tag) 332 | ) 333 | 334 | # Read the object using the specific reader: 335 | return reader(cursor) 336 | finally: 337 | if cursor is not None and cursor != source: 338 | cursor.close() 339 | -------------------------------------------------------------------------------- /sdk/lib/ovirtsdk4/service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from ovirtsdk4 import AuthError 20 | from ovirtsdk4 import Error 21 | from ovirtsdk4 import NotFoundError 22 | from ovirtsdk4 import http 23 | from ovirtsdk4 import reader 24 | from ovirtsdk4 import types 25 | from ovirtsdk4 import writer 26 | 27 | 28 | class Future(object): 29 | """ 30 | Instances of this class are returned for operations that specify the 31 | `wait=False` parameter. 32 | """ 33 | def __init__(self, connection, context, code): 34 | """ 35 | Creates a new future result. 36 | 37 | :param connection: The connection to be used by this future. 38 | 39 | :param context: The request that this future will wait for when the 40 | `wait` method is called. 41 | 42 | :param code: The function that will be executed to check the response, 43 | and to convert its body into the right type of object. 44 | """ 45 | self._connection = connection 46 | self._context = context 47 | self._code = code 48 | 49 | def wait(self): 50 | """ 51 | Waits till the result of the operation that created this future is 52 | available. 53 | """ 54 | response = self._connection.wait(self._context) 55 | return self._code(response) 56 | 57 | 58 | class Service(object): 59 | """ 60 | This is the base class for all the services of the SDK. It contains the 61 | utility methods used by all of them. 62 | """ 63 | 64 | def __init__(self, connection, path): 65 | """ 66 | Creates a new service that will use the given connection and path. 67 | """ 68 | self._connection = connection 69 | self._path = path 70 | 71 | @staticmethod 72 | def _raise_error(response, detail=None): 73 | """ 74 | Creates and raises an error containing the details of the given HTTP 75 | response and fault. 76 | 77 | This method is intended for internal use by other components of the 78 | SDK. Refrain from using it directly, as backwards compatibility isn't 79 | guaranteed. 80 | """ 81 | fault = detail if isinstance(detail, types.Fault) else None 82 | 83 | msg = '' 84 | if fault: 85 | if fault.reason: 86 | if msg: 87 | msg += ' ' 88 | msg = msg + 'Fault reason is "%s".' % fault.reason 89 | if fault.detail: 90 | if msg: 91 | msg += ' ' 92 | msg = msg + 'Fault detail is "%s".' % fault.detail 93 | if response: 94 | if response.code: 95 | if msg: 96 | msg += ' ' 97 | msg = msg + 'HTTP response code is %s.' % response.code 98 | if response.message: 99 | if msg: 100 | msg += ' ' 101 | msg = msg + 'HTTP response message is "%s".' % response.message 102 | 103 | if isinstance(detail, str): 104 | if msg: 105 | msg += ' ' 106 | msg = msg + detail + '.' 107 | 108 | class_ = Error 109 | if response is not None: 110 | if response.code in [401, 403]: 111 | class_ = AuthError 112 | elif response.code == 404: 113 | class_ = NotFoundError 114 | 115 | error = class_(msg) 116 | error.code = response.code if response else None 117 | error.fault = fault 118 | raise error 119 | 120 | def _check_fault(self, response): 121 | """ 122 | Reads the response body assuming that it contains a fault message, 123 | converts it to an exception and raises it. 124 | 125 | This method is intended for internal use by other 126 | components of the SDK. Refrain from using it directly, 127 | as backwards compatibility isn't guaranteed. 128 | """ 129 | 130 | body = self._internal_read_body(response) 131 | if isinstance(body, types.Fault): 132 | self._raise_error(response, body) 133 | elif isinstance(body, types.Action) and body.fault: 134 | self._raise_error(response, body.fault) 135 | raise Error("Expected a fault, but got %s" % type(body).__name__) 136 | 137 | def _check_action(self, response): 138 | """ 139 | Reads the response body assuming that it contains an action, checks if 140 | it contains an fault, and if it does converts it to an exception and 141 | raises it. 142 | 143 | This method is intended for internal use by other 144 | components of the SDK. Refrain from using it directly, 145 | as backwards compatibility isn't guaranteed. 146 | """ 147 | 148 | body = self._internal_read_body(response) 149 | if isinstance(body, types.Fault): 150 | self._raise_error(response, body) 151 | elif isinstance(body, types.Action) and body.fault is not None: 152 | self._raise_error(response, body.fault) 153 | elif isinstance(body, types.Action): 154 | return body 155 | else: 156 | raise Error( 157 | "Expected a fault or action, but got %s" % ( 158 | type(body).__name__ 159 | ) 160 | ) 161 | return body 162 | 163 | @staticmethod 164 | def _check_types(tuples): 165 | """ 166 | Receives a list of tuples with three elements: the name of a 167 | parameter, its value and its expected type. For each tuple it 168 | checks that the value is either `None` or a valid value of 169 | the given types. If any of the checks fails, it raises an 170 | exception. 171 | 172 | This method is intended for internal use by other 173 | components of the SDK. Refrain from using it directly, 174 | as backwards compatibility isn't guaranteed. 175 | """ 176 | 177 | messages = [] 178 | for name, value, expected in tuples: 179 | if value is not None: 180 | actual = type(value) 181 | if actual != expected: 182 | messages.append(( 183 | "The '{name}' parameter should be of type " 184 | "'{expected}', but it is of type \'{actual}\'." 185 | ).format( 186 | name=name, 187 | expected=expected.__name__, 188 | actual=actual.__name__, 189 | )) 190 | if len(messages) > 0: 191 | raise TypeError(' '.join(messages)) 192 | 193 | def _internal_get(self, headers=None, query=None, wait=None): 194 | """ 195 | Executes an `get` method. 196 | """ 197 | # Populate the headers: 198 | headers = headers or {} 199 | 200 | # Send the request: 201 | request = http.Request(method='GET', path=self._path, query=query, headers=headers) 202 | context = self._connection.send(request) 203 | 204 | def callback(response): 205 | if response.code in [200]: 206 | return self._internal_read_body(response) 207 | else: 208 | self._check_fault(response) 209 | 210 | future = Future(self._connection, context, callback) 211 | return future.wait() if wait else future 212 | 213 | def _internal_add(self, object, headers=None, query=None, wait=None): 214 | """ 215 | Executes an `add` method. 216 | """ 217 | # Populate the headers: 218 | headers = headers or {} 219 | 220 | # Send the request and wait for the response: 221 | request = http.Request(method='POST', path=self._path, query=query, headers=headers) 222 | request.body = writer.Writer.write(object, indent=True) 223 | context = self._connection.send(request) 224 | 225 | def callback(response): 226 | if response.code in [200, 201, 202]: 227 | return self._internal_read_body(response) 228 | else: 229 | self._check_fault(response) 230 | 231 | future = Future(self._connection, context, callback) 232 | return future.wait() if wait else future 233 | 234 | def _internal_update(self, object, headers=None, query=None, wait=None): 235 | """ 236 | Executes an `update` method. 237 | """ 238 | # Populate the headers: 239 | headers = headers or {} 240 | 241 | # Send the request and wait for the response: 242 | request = http.Request(method='PUT', path=self._path, query=query, headers=headers) 243 | request.body = writer.Writer.write(object, indent=True) 244 | context = self._connection.send(request) 245 | 246 | def callback(response): 247 | if response.code in [200]: 248 | return self._internal_read_body(response) 249 | else: 250 | self._check_fault(response) 251 | 252 | future = Future(self._connection, context, callback) 253 | return future.wait() if wait else future 254 | 255 | def _internal_remove(self, headers=None, query=None, wait=None): 256 | """ 257 | Executes an `remove` method. 258 | """ 259 | # Populate the headers: 260 | headers = headers or {} 261 | 262 | # Send the request and wait for the response: 263 | request = http.Request(method='DELETE', path=self._path, query=query, headers=headers) 264 | context = self._connection.send(request) 265 | 266 | def callback(response): 267 | if response.code not in [200]: 268 | self._check_fault(response) 269 | 270 | future = Future(self._connection, context, callback) 271 | return future.wait() if wait else future 272 | 273 | def _internal_action(self, action, path, member=None, headers=None, query=None, wait=None): 274 | """ 275 | Executes an action method. 276 | """ 277 | # Populate the headers: 278 | headers = headers or {} 279 | 280 | # Send the request and wait for the response: 281 | request = http.Request( 282 | method='POST', 283 | path='%s/%s' % (self._path, path), 284 | query=query, 285 | headers=headers, 286 | ) 287 | request.body = writer.Writer.write(action, indent=True) 288 | context = self._connection.send(request) 289 | 290 | def callback(response): 291 | if response.code in [200, 201, 202]: 292 | result = self._check_action(response) 293 | if member: 294 | return getattr(result, member) 295 | else: 296 | self._check_fault(response) 297 | 298 | future = Future(self._connection, context, callback) 299 | return future.wait() if wait else future 300 | 301 | def _internal_read_body(self, response): 302 | """ 303 | Checks the content type of the given response, and if it is XML, as 304 | expected, reads the body and converts it to an object. If it isn't 305 | XML, then it raises an exception. 306 | 307 | `response`:: The HTTP response to check. 308 | """ 309 | # First check if the response body is empty, as it makes no sense to check the content type if there is 310 | # no body: 311 | if not response.body: 312 | self._raise_error(response) 313 | 314 | # Check the content type, as otherwise the parsing will fail, and the resulting error message won't be explicit 315 | # about the cause of the problem: 316 | self._connection.check_xml_content_type(response) 317 | 318 | # Parse the XML and generate the SDK object: 319 | return reader.Reader.read(response.body) 320 | -------------------------------------------------------------------------------- /sdk/tests/test_vm_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Copyright oVirt Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import ovirtsdk4 20 | import ovirtsdk4.types as types 21 | import unittest 22 | 23 | import pytest 24 | 25 | from .server import TestServer 26 | 27 | 28 | class VmServiceTest(unittest.TestCase): 29 | 30 | @classmethod 31 | def setup_class(cls): 32 | cls.server = TestServer() 33 | cls.server.start_server() 34 | cls.connection = cls.server.connection() 35 | system_service = cls.connection.system_service() 36 | cls.vms_service = system_service.vms_service() 37 | 38 | @classmethod 39 | def teardown_class(cls): 40 | cls.connection.close() 41 | cls.server.stop_server() 42 | 43 | def test_get_service(self): 44 | """ 45 | Check that reference to vm service is not none 46 | """ 47 | assert self.vms_service is not None 48 | 49 | def test_get_list_of_vms(self): 50 | """ 51 | Test returning empty vm list 52 | """ 53 | self.server.set_xml_response("vms", 200, "") 54 | vms = self.vms_service.list() 55 | assert vms is not None 56 | assert vms == [] 57 | 58 | def test_get_list_of_storage_domains_with_search(self): 59 | """ 60 | Test returning empty vms list 61 | """ 62 | self.server.set_xml_response("vms", 200, "") 63 | vms = self.vms_service.list(search="name=ugly") 64 | assert vms is not None 65 | assert vms == [] 66 | 67 | def test_get_vm_by_id(self): 68 | """ 69 | Test we don't get null vm service for existing 70 | vm id and correct object 71 | """ 72 | self.server.set_xml_response( 73 | path="vms/123", 74 | code=200, 75 | body="testvm" 76 | ) 77 | dc = self.vms_service.vm_service("123").get() 78 | assert dc.id == "123" 79 | assert dc.name == "testvm" 80 | 81 | def test_add_vm_with_clone_parameter(self): 82 | """ 83 | Test when adding clone vm the query with this parameter is sent. 84 | """ 85 | self.server.set_xml_response("vms", 201, "") 86 | self.vms_service.add(types.Vm(), clone=True) 87 | assert self.server.last_request_query == 'clone=true' 88 | 89 | def test_add_vm_with_clone_and_clone_permissions_parameters(self): 90 | """ 91 | Test when adding vm clone and clone_permissions the query with 92 | those parameters is sent. 93 | """ 94 | self.server.set_xml_response("vms", 201, "") 95 | self.vms_service.add( 96 | types.Vm(), 97 | clone=True, 98 | clone_permissions=True 99 | ) 100 | assert ( 101 | self.server.last_request_query == 'clone=true&clone_permissions=true' 102 | ) 103 | 104 | def test_add_vm_from_scratch_with_clone_parameter(self): 105 | """ 106 | Test when adding clone vm from scratch the query with this 107 | parameter is sent. 108 | """ 109 | self.server.set_xml_response("vms", 201, "") 110 | self.vms_service.add_from_scratch(types.Vm(), clone=True) 111 | assert self.server.last_request_query == 'clone=true' 112 | 113 | def test_add_vm_from_scratch_with_clone_and_clone_permissions_parameters(self): 114 | """ 115 | Test when adding vm clone and clone_permissions from scarch 116 | the query with those parameters is sent. 117 | """ 118 | self.server.set_xml_response("vms", 201, "") 119 | self.vms_service.add_from_scratch( 120 | types.Vm(), 121 | clone=True, 122 | clone_permissions=True 123 | ) 124 | assert ( 125 | self.server.last_request_query == 'clone=true&clone_permissions=true' 126 | ) 127 | 128 | def test_response_200_not_raise_exception(self): 129 | """ 130 | Test when server return response with 200 code, 131 | the SDK don't raise exception. 132 | """ 133 | self.server.set_xml_response("vms", 200, "") 134 | self.vms_service.add(types.Vm()) 135 | 136 | def test_response_201_not_raise_exception(self): 137 | """ 138 | Test when server return response with 201 code, 139 | the SDK don't raise exception. 140 | """ 141 | self.server.set_xml_response("vms", 201, "") 142 | self.vms_service.add(types.Vm()) 143 | 144 | def test_response_202_not_raise_exception(self): 145 | """ 146 | Test when server return response with 202 code, 147 | the SDK don't raise exception. 148 | """ 149 | self.server.set_xml_response("vms", 202, "") 150 | self.vms_service.add(types.Vm()) 151 | 152 | def test_404_error_is_raise_when_vm_dont_exist(self): 153 | """ 154 | Test that get raises an 404 error if the VM does not exist 155 | """ 156 | self.server.set_xml_response("vms/123", 404, "") 157 | with pytest.raises(ovirtsdk4.NotFoundError) as context: 158 | self.vms_service.vm_service('123').get() 159 | assert "404" in str(context.value) 160 | 161 | def test_error_code_is_returned_in_exception(self): 162 | """ 163 | Test that 404 error code is returned in exception if VM don't exist 164 | """ 165 | self.server.set_xml_response("vms/123", 404, "") 166 | with pytest.raises(ovirtsdk4.NotFoundError) as context: 167 | self.vms_service.vm_service('123').get() 168 | assert context.value.code == 404 169 | 170 | def test_start_with_custom_parameter(self): 171 | """ 172 | Test that sending one parameter a request is sent with that parameter. 173 | """ 174 | self.server.set_xml_response("vms/123/start", 200, "") 175 | self.vms_service.vm_service("123").start(query={'my': 'value'}) 176 | assert self.server.last_request_query == 'my=value' 177 | 178 | def test_start_with_two_custom_parameters(self): 179 | """ 180 | Test that sending two parameters a request is sent with that two parameters. 181 | """ 182 | self.server.set_xml_response("vms/123/start", 200, "") 183 | self.vms_service.vm_service("123").start( 184 | query={'my': 'value', 'your': 'value'} 185 | ) 186 | assert self.server.last_request_query == 'my=value&your=value' 187 | 188 | def test_start_with_custom_header(self): 189 | """ 190 | Test that sending one header a request is sent with that header. 191 | """ 192 | self.server.set_xml_response("vms/123/start", 200, "") 193 | self.vms_service.vm_service("123").start(headers={'my': 'value'}) 194 | assert self.server.last_request_headers.get('my') == 'value' 195 | 196 | def test_start_with_two_custom_headers(self): 197 | """ 198 | Test that sending two headers a request is sent with that two headers. 199 | """ 200 | self.server.set_xml_response("vms/123/start", 200, "") 201 | self.vms_service.vm_service("123").start( 202 | headers={'my': 'value', 'your': 'value'} 203 | ) 204 | assert self.server.last_request_headers.get('my') == 'value' 205 | assert self.server.last_request_headers.get('your') == 'value' 206 | 207 | def test_add_vm_with_custom_parameter(self): 208 | """ 209 | Test that adding a VM with one parameter a request is sent with that parameter. 210 | """ 211 | self.server.set_xml_response("vms", 201, "") 212 | self.vms_service.add(types.Vm(), query={'my': 'value'}) 213 | assert self.server.last_request_query == 'my=value' 214 | 215 | def test_add_vm_with_two_custom_parameters(self): 216 | """ 217 | Test that adding a VM with two parameters a request is sent with that two parameters. 218 | """ 219 | self.server.set_xml_response("vms", 201, "") 220 | self.vms_service.add(types.Vm(), query={'my': 'value', 'your': 'value'}) 221 | assert self.server.last_request_query == 'my=value&your=value' 222 | 223 | def test_add_vm_with_custom_header(self): 224 | """ 225 | Test that adding a VM with one header a request is sent with that header. 226 | """ 227 | self.server.set_xml_response("vms", 201, "") 228 | self.vms_service.add(types.Vm(), headers={'my': 'value'}) 229 | assert self.server.last_request_headers.get('my') == 'value' 230 | 231 | def test_add_vm_with_two_custom_headers(self): 232 | """ 233 | Test that adding a VM with two headers a request is sent with that two headers. 234 | """ 235 | self.server.set_xml_response("vms", 201, "") 236 | self.vms_service.add(types.Vm(), headers={'my': 'value', 'your': 'value'}) 237 | assert self.server.last_request_headers.get('my') == 'value' 238 | assert self.server.last_request_headers.get('your') == 'value' 239 | 240 | def test_add_vm_with_global_header(self): 241 | """ 242 | Test that adding a VM with global header a request is sent with that header. 243 | """ 244 | connection = self.server.connection(headers={'my': 'value'}) 245 | vms_service = connection.system_service().vms_service() 246 | self.server.set_xml_response("vms", 201, "") 247 | vms_service.add(types.Vm()) 248 | assert self.server.last_request_headers.get('my') == 'value' 249 | 250 | def test_start_vm_with_global_header(self): 251 | """ 252 | Test that starting a VM with header a request is sent with that header. 253 | """ 254 | connection = self.server.connection(headers={'my': 'value'}) 255 | vms_service = connection.system_service().vms_service() 256 | self.server.set_xml_response("vms/123/start", 200, "") 257 | vms_service.vm_service("123").start() 258 | assert self.server.last_request_headers.get('my') == 'value' 259 | 260 | def test_add_vm_with_global_header_overridden(self): 261 | """ 262 | Test that adding a VM with global header set and a request header set, 263 | the header is overridden by request header. 264 | """ 265 | connection = self.server.connection(headers={'my': 'value'}) 266 | vms_service = connection.system_service().vms_service() 267 | self.server.set_xml_response("vms", 201, "") 268 | vms_service.add(types.Vm(), headers={'my': 'overridden'}) 269 | assert self.server.last_request_headers.get('my') == 'overridden' 270 | 271 | def test_when_the_server_return_fault_it_raises_error_with_fault(self): 272 | """ 273 | Test that when server return a fault the SDK will raise an exception 274 | with fault object. 275 | """ 276 | self.server.set_xml_response( 277 | path='vms/123/start', 278 | code=201, 279 | body=( 280 | '' 281 | '' 282 | 'myreason' 283 | 'mydetail' 284 | '' 285 | '' 286 | ) 287 | ) 288 | with pytest.raises(ovirtsdk4.Error) as context: 289 | self.vms_service.vm_service('123').start() 290 | assert 'myreason' in str(context.value) 291 | assert context.value.fault.reason == 'myreason' 292 | assert context.value.fault.detail == 'mydetail' 293 | 294 | def test_the_server_return_fault_without_action_it_raises_error(self): 295 | """ 296 | Test that when server return a fault without action the SDK will raise 297 | an exception with fault object. 298 | """ 299 | self.server.set_xml_response( 300 | path='vms/123/start', 301 | code=400, 302 | body=( 303 | '' 304 | 'myreason' 305 | 'mydetail' 306 | '' 307 | ) 308 | ) 309 | with pytest.raises(ovirtsdk4.Error) as context: 310 | self.vms_service.vm_service('123').start() 311 | assert 'myreason' in str(context.value) 312 | assert context.value.fault.reason == 'myreason' 313 | assert context.value.fault.detail == 'mydetail' 314 | --------------------------------------------------------------------------------