From 1cf85402fc215880eb78edcebe14f2db39b2c095 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Mon, 20 Feb 2023 11:36:27 -0500 Subject: [PATCH 01/15] Issue 217 | Docs: getting started 3 level tutorials --- docs/getting_started/01_intro_level_1.ipynb | 240 +++++++++++++++ docs/getting_started/01_intro_level_2.ipynb | 238 +++++++++++++++ docs/getting_started/01_intro_level_3.ipynb | 279 ++++++++++++++++++ docs/getting_started/index.rst | 4 + docs/getting_started/source_files/__init__.py | 0 .../source_files/gs_level_1.py | 14 + .../source_files/gs_level_2.py | 69 +++++ .../source_files/gs_level_3.py | 50 ++++ docs/index.rst | 15 + 9 files changed, 909 insertions(+) create mode 100644 docs/getting_started/01_intro_level_1.ipynb create mode 100644 docs/getting_started/01_intro_level_2.ipynb create mode 100644 docs/getting_started/01_intro_level_3.ipynb create mode 100644 docs/getting_started/index.rst create mode 100644 docs/getting_started/source_files/__init__.py create mode 100644 docs/getting_started/source_files/gs_level_1.py create mode 100644 docs/getting_started/source_files/gs_level_2.py create mode 100644 docs/getting_started/source_files/gs_level_3.py diff --git a/docs/getting_started/01_intro_level_1.ipynb b/docs/getting_started/01_intro_level_1.ipynb new file mode 100644 index 000000000..2c893b49a --- /dev/null +++ b/docs/getting_started/01_intro_level_1.ipynb @@ -0,0 +1,240 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "66030e20-b384-4dcf-9c5f-7664f7ad1693", + "metadata": {}, + "source": [ + "# Getting started - level 1\n", + "\n", + "Let's write `Hello World` program using quantum serverless. \n", + "\n", + "We will start with writing code for our program and saving it to `./source_files/gs_level_1.py` file. It will be simple hello world qiskit example.\n", + "\n", + "```python\n", + "# source_files/gs_level_1.py\n", + "\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.primitives import Sampler\n", + "\n", + "circuit = QuantumCircuit(2)\n", + "circuit.h(0)\n", + "circuit.cx(0, 1)\n", + "circuit.measure_all()\n", + "circuit.draw()\n", + "\n", + "sampler = Sampler()\n", + "\n", + "quasi_dists = sampler.run(circuit).result().quasi_dists\n", + "\n", + "print(f\"Quasi distribution: {quasi_dists[0]}\")\n", + "```\n", + "\n", + "Next we need to run this program. For that we need to import necessary modules and configure QuantumServerless client. We are doing so by providing name and host for deployed infrastructure." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "81dd7807-7180-4b87-bbf9-832b7cf29d69", + "metadata": {}, + "outputs": [], + "source": [ + "from quantum_serverless import QuantumServerless, Program" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "acdec789-4967-48ee-8f6c-8d2b0ff57e91", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless = QuantumServerless({\n", + " \"providers\": [{\n", + " \"name\": \"docker\",\n", + " \"compute_resource\": {\n", + " \"name\": \"docker\",\n", + " \"host\": \"localhost\",\n", + " }\n", + " }]\n", + "})\n", + "serverless" + ] + }, + { + "cell_type": "markdown", + "id": "4dd85621-9ab0-4f34-9ab4-07ad773c5e00", + "metadata": {}, + "source": [ + "Now we need to run our program file, by creating an instance of `Program` and calling `run_program` method of our `QuantumServerless` client.\n", + "\n", + "`Program` accepts couple of required parameters:\n", + "- name - name of the program\n", + "- entrypoint - name of python file you want to execute\n", + "- working_dir - folder where your script is located. This is optional parameter and will be current folder by default. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d51df836-3f22-467c-b637-5803145d5d8a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "program = Program(\n", + " name=\"Getting started program level 1\",\n", + " entrypoint=\"gs_level_1.py\",\n", + " working_dir=\"./source_files/\"\n", + ")\n", + "\n", + "job = serverless.run_program(program)\n", + "job" + ] + }, + { + "cell_type": "markdown", + "id": "39ee31d2-3553-4e19-bcb9-4cccd0df0e4c", + "metadata": {}, + "source": [ + "As result of `run_program` call we get `Job` which has `status` method to check status of program execution, `logs` to get logs of execution." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cc7ccea6-bbae-4184-ba7f-67b6c20a0b0b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.status()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ca76abfa-2ff5-425b-a225-058d91348e8b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Quasi distribution: {0: 0.4999999999999999, 3: 0.4999999999999999}\\n'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.logs()" + ] + }, + { + "cell_type": "markdown", + "id": "3b1113ef-e8ad-4ed9-b07b-9da2f2b9ea1c", + "metadata": {}, + "source": [ + "Also this object has `job_id` property that can be used if you want to access job results later.\n", + "To do so we need to call `get_job_by_id` method of `QuantumServerless` client." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f942b76d-596c-4384-8f36-e5f73e72cefd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'qs_76b8db40-6150-48bf-b971-ac0b13db12b5'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.job_id" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "45e2927f-655b-47a4-8003-f16e5ba0a1cd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless.get_job_by_id(job.job_id)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/getting_started/01_intro_level_2.ipynb b/docs/getting_started/01_intro_level_2.ipynb new file mode 100644 index 000000000..ccbae9b24 --- /dev/null +++ b/docs/getting_started/01_intro_level_2.ipynb @@ -0,0 +1,238 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3ff90cec-1b42-4744-9400-eb5a4bd1ef3a", + "metadata": {}, + "source": [ + "# Getting started - level 2\n", + "\n", + "In this tutorial we will explore a little bit more advanced example of a program that require some configuration, requirements setup, etc. \n", + "\n", + "Again we will start with writing code for our program and saving it to `./source_files/gs_level_2.py` file.\n", + "This time it will be VQE example from [Qiskit documentation](https://qiskit.org/documentation/nature/tutorials/07_leveraging_qiskit_runtime.html) and we also introduce dependency management and arguments to our programs.\n", + "\n", + "```python\n", + "# source_files/gs_level_2.py\n", + "\n", + "import argparse\n", + "\n", + "from qiskit_nature.units import DistanceUnit\n", + "from qiskit_nature.second_q.drivers import PySCFDriver\n", + "from qiskit_nature.second_q.mappers import QubitConverter\n", + "from qiskit_nature.second_q.mappers import ParityMapper\n", + "from qiskit_nature.second_q.properties import ParticleNumber\n", + "from qiskit_nature.second_q.transformers import ActiveSpaceTransformer\n", + "from qiskit.algorithms.minimum_eigensolvers import NumPyMinimumEigensolver\n", + "from qiskit_nature.second_q.algorithms.ground_state_solvers import GroundStateEigensolver\n", + "from qiskit.circuit.library import EfficientSU2\n", + "import numpy as np\n", + "from qiskit.utils import algorithm_globals\n", + "from qiskit.algorithms.optimizers import SPSA\n", + "from qiskit.algorithms.minimum_eigensolvers import VQE\n", + "from qiskit.primitives import Estimator\n", + "\n", + "\n", + "def run(bond_distance: float = 2.5):\n", + " driver = PySCFDriver(\n", + " atom=f\"Li 0 0 0; H 0 0 {bond_distance}\",\n", + " basis=\"sto3g\",\n", + " charge=0,\n", + " spin=0,\n", + " unit=DistanceUnit.ANGSTROM,\n", + " )\n", + " problem = driver.run()\n", + "\n", + " active_space_trafo = ActiveSpaceTransformer(\n", + " num_electrons=problem.num_particles, num_spatial_orbitals=3\n", + " )\n", + " problem = active_space_trafo.transform(problem)\n", + " qubit_converter = QubitConverter(ParityMapper(), two_qubit_reduction=True)\n", + "\n", + " ansatz = EfficientSU2(num_qubits=4, reps=1, entanglement=\"linear\", insert_barriers=True)\n", + "\n", + " np.random.seed(5)\n", + " algorithm_globals.random_seed = 5\n", + "\n", + "\n", + " optimizer = SPSA(maxiter=100)\n", + " initial_point = np.random.random(ansatz.num_parameters)\n", + "\n", + " estimator = Estimator()\n", + " local_vqe = VQE(\n", + " estimator,\n", + " ansatz,\n", + " optimizer,\n", + " initial_point=initial_point,\n", + " )\n", + "\n", + " local_vqe_groundstate_solver = GroundStateEigensolver(qubit_converter, local_vqe)\n", + " local_vqe_result = local_vqe_groundstate_solver.solve(problem)\n", + "\n", + " print(local_vqe_result)\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " parser = argparse.ArgumentParser()\n", + " parser.add_argument(\n", + " \"--bond_length\",\n", + " help=\"Bond length in Angstrom.\",\n", + " default=2.5,\n", + " type=float,\n", + " )\n", + " args = parser.parse_args()\n", + "\n", + " print(f\"Running for bond length {args.bond_length}.\")\n", + " run(args.bond_length)\n", + "\n", + "```\n", + "\n", + "As you can see here we used couple of additional things compared to `getting started level 1`. \n", + "\n", + "First we are using `qiskit-nature` module and `pyscf` extension. \n", + "We also using argument parsing to accept arguments to our program. In this case argument is `bond_length`. \n", + "\n", + "Next we need to run this program. For that we need to import necessary modules and configure `QuantumServerless` client. We are doing so by providing name and host for deployed infrastructure.\n", + "\n", + "In addition to that we will provide additional `dependencies` and `arguments` to our `Program` construction.\n", + "- `dependencies` parameter will install provided libraries to run our script\n", + "- `arguments` parameter is a dictionary with arguments that will be passed for script execution" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "79434a17-1222-4d04-a81a-8140ed630ed6", + "metadata": {}, + "outputs": [], + "source": [ + "from quantum_serverless import QuantumServerless, Program" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b6ec8969-8c3d-4b7f-8c4c-adc6dbb9c59f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless = QuantumServerless({\n", + " \"providers\": [{\n", + " \"name\": \"docker\",\n", + " \"compute_resource\": {\n", + " \"name\": \"docker\",\n", + " \"host\": \"localhost\",\n", + " }\n", + " }]\n", + "})\n", + "serverless" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3ee09b31-4c7f-4ff3-af8f-294e4256793e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "program = Program(\n", + " name=\"Getting started program level 2\",\n", + " entrypoint=\"gs_level_2.py\",\n", + " working_dir=\"./source_files\",\n", + " dependencies=[\"qiskit-nature\", \"qiskit-nature[pyscf]\"],\n", + " arguments={\n", + " \"bond_length\": 2.55\n", + " }\n", + ")\n", + "\n", + "job = serverless.run_program(program)\n", + "job" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7e3b0cc7-2f08-4b69-a266-bbbe4e9a6c59", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running for bond length 2.55.\n", + "=== GROUND STATE ENERGY ===\n", + " \n", + "* Electronic ground state energy (Hartree): -8.211426457622\n", + " - computed part: -8.211426457622\n", + " - ActiveSpaceTransformer extracted energy part: 0.0\n", + "~ Nuclear repulsion energy (Hartree): 0.622561424612\n", + "> Total ground state energy (Hartree): -7.58886503301\n", + " \n", + "=== MEASURED OBSERVABLES ===\n", + " \n", + " 0: # Particles: 3.997 S: 0.436 S^2: 0.626 M: 0.001\n", + " \n", + "=== DIPOLE MOMENTS ===\n", + " \n", + "~ Nuclear dipole moment (a.u.): [0.0 0.0 4.81880162]\n", + " \n", + " 0: \n", + " * Electronic dipole moment (a.u.): [0.0 0.0 1.53218971]\n", + " - computed part: [0.0 0.0 1.53218971]\n", + " - ActiveSpaceTransformer extracted energy part: [0.0 0.0 0.0]\n", + " > Dipole moment (a.u.): [0.0 0.0 3.28661191] Total: 3.28661191\n", + " (debye): [0.0 0.0 8.35373344] Total: 8.35373344\n", + " \n", + "\n" + ] + } + ], + "source": [ + "print(job.logs())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/getting_started/01_intro_level_3.ipynb b/docs/getting_started/01_intro_level_3.ipynb new file mode 100644 index 000000000..f664ffb9e --- /dev/null +++ b/docs/getting_started/01_intro_level_3.ipynb @@ -0,0 +1,279 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "73944cc6-e22d-43ae-8716-78bb00360a0f", + "metadata": {}, + "source": [ + "# Getting started - level 3\n", + "\n", + "In this tutorial we will explore a little bit more advanced example of a program that require some configuration, requirements setup, etc. \n", + "\n", + "Again we will start with writing code for our program and saving it to `./source_files/gs_level_3.py` file.\n", + "This time it will be running estimator as parallel functions and saving results to shared state. \n", + "\n", + "```python\n", + "# source_files/gs_level_3.py\n", + "\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit.random import random_circuit\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit.primitives import Estimator\n", + "\n", + "from quantum_serverless import QuantumServerless, run_qiskit_remote, get, put\n", + "from quantum_serverless.core.state import RedisStateHandler\n", + "\n", + "# 1. let's annotate out function to convert it\n", + "# to function that can be executed remotely\n", + "# using `run_qiskit_remote` decorator\n", + "@run_qiskit_remote()\n", + "def my_function(circuit: QuantumCircuit, obs: SparsePauliOp):\n", + " return Estimator().run([circuit], [obs]).result().values\n", + "\n", + "\n", + "# 2. Next let's create out serverless object to control\n", + "# where our remote function will be executed\n", + "serverless = QuantumServerless()\n", + "\n", + "# 2.1 (Optional) state handler to write/read results in/out of job\n", + "state_handler = RedisStateHandler(\"redis\", 6379)\n", + "\n", + "circuits = [random_circuit(2, 2) for _ in range(3)]\n", + "\n", + "# 3. create serverless context\n", + "with serverless.context():\n", + " # 4. let's put some shared objects into remote storage that will be shared among all executions\n", + " obs_ref = put(SparsePauliOp([\"ZZ\"]))\n", + "\n", + " # 4. run our function and get back reference to it\n", + " # as now our function it remote one\n", + " function_reference = my_function(circuits[0], obs_ref)\n", + "\n", + " # 4.1 or we can run N of them in parallel (for all circuits)\n", + " function_references = [my_function(circ, obs_ref) for circ in circuits]\n", + "\n", + " # 5. to get results back from reference\n", + " # we need to call `get` on function reference\n", + " single_result = get(function_reference)\n", + " parallel_result = get(function_references)\n", + " print(\"Single execution:\", single_result)\n", + " print(\"N parallel executions:\", parallel_result)\n", + "\n", + " # 5.1 (Optional) write results to state.\n", + " state_handler.set(\"result\", {\n", + " \"status\": \"ok\",\n", + " \"single\": single_result.tolist(),\n", + " \"parallel_result\": [entry.tolist() for entry in parallel_result]\n", + " })\n", + "```\n", + "\n", + "As you can see we move to advanced section of using serverless. \n", + "\n", + "Here we are using `run_qiskit_remote` decorator to convert our function to parallel one. \n", + "With that `my_function` is converted into remote call (as a result you will be getting function pointer) and in order to fetch results of this function we need to call `get` function.\n", + "\n", + "Moreover, we are using `RedisStateHandler` in order to save results into state storage, so we can retrieve it later after program execution.\n", + "\n", + "Next we need to run this program. For that we need to import necessary modules and configure QuantumServerless client. We are doing so by providing name and host for deployed infrastructure." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9130c64a-1e7f-4d08-afff-b2905b2d95ad", + "metadata": {}, + "outputs": [], + "source": [ + "from quantum_serverless import QuantumServerless, Program\n", + "from quantum_serverless.core.state import RedisStateHandler" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0f22daae-9f0e-4f7a-8a1f-5ade989d8be9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serverless = QuantumServerless({\n", + " \"providers\": [{\n", + " \"name\": \"docker\",\n", + " \"compute_resource\": {\n", + " \"name\": \"docker\",\n", + " \"host\": \"localhost\",\n", + " }\n", + " }]\n", + "})\n", + "serverless" + ] + }, + { + "cell_type": "markdown", + "id": "1f2e4c59-6a45-4e8e-919e-89e66c9727a1", + "metadata": {}, + "source": [ + "We will create instance of state handler to fetch results from program after execution" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "28a77e08-e542-4259-af67-1c70b8498f68", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state_handler = RedisStateHandler(\"localhost\", 6379)\n", + "state_handler" + ] + }, + { + "cell_type": "markdown", + "id": "3321b4a0-b60d-433a-992a-79e5868d309b", + "metadata": {}, + "source": [ + "Run program" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f556dd85-35da-48d1-9ae1-f04a386544d9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "program = Program(\n", + " name=\"Advanced program\",\n", + " entrypoint=\"gs_level_3.py\",\n", + " working_dir=\"./source_files/\"\n", + ")\n", + "\n", + "job = serverless.run_program(program)\n", + "job" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2de3fd64-9010-48d9-ac7c-f46a7b36ba81", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.status()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d6586e7a-388b-42cc-a860-abd4f6d514b9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Single execution: [1.]\n", + "N parallel executions: [array([1.]), array([0.]), array([-1.])]\n", + "\n" + ] + } + ], + "source": [ + "print(job.logs())" + ] + }, + { + "cell_type": "markdown", + "id": "29336f0b-ffcf-4cdb-931c-11faf09f15ff", + "metadata": {}, + "source": [ + "With `state_handler` as can fetch results that we wrote inside the program." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "1fb8931f-c8e2-49dd-923f-16fa3a7a5feb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'status': 'ok', 'single': [1.0], 'parallel_result': [[1.0], [0.0], [-1.0]]}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state_handler.get(\"result\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/getting_started/index.rst b/docs/getting_started/index.rst new file mode 100644 index 000000000..b7aba0a31 --- /dev/null +++ b/docs/getting_started/index.rst @@ -0,0 +1,4 @@ +.. nbgallery:: + :glob: + + * diff --git a/docs/getting_started/source_files/__init__.py b/docs/getting_started/source_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/getting_started/source_files/gs_level_1.py b/docs/getting_started/source_files/gs_level_1.py new file mode 100644 index 000000000..193eba6f2 --- /dev/null +++ b/docs/getting_started/source_files/gs_level_1.py @@ -0,0 +1,14 @@ +from qiskit import QuantumCircuit +from qiskit.primitives import Sampler + +circuit = QuantumCircuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure_all() +circuit.draw() + +sampler = Sampler() + +quasi_dists = sampler.run(circuit).result().quasi_dists + +print(f"Quasi distribution: {quasi_dists[0]}") diff --git a/docs/getting_started/source_files/gs_level_2.py b/docs/getting_started/source_files/gs_level_2.py new file mode 100644 index 000000000..e6c30dc3c --- /dev/null +++ b/docs/getting_started/source_files/gs_level_2.py @@ -0,0 +1,69 @@ +import argparse + +from qiskit_nature.units import DistanceUnit +from qiskit_nature.second_q.drivers import PySCFDriver +from qiskit_nature.second_q.mappers import QubitConverter +from qiskit_nature.second_q.mappers import ParityMapper +from qiskit_nature.second_q.properties import ParticleNumber +from qiskit_nature.second_q.transformers import ActiveSpaceTransformer +from qiskit.algorithms.minimum_eigensolvers import NumPyMinimumEigensolver +from qiskit_nature.second_q.algorithms.ground_state_solvers import GroundStateEigensolver +from qiskit.circuit.library import EfficientSU2 +import numpy as np +from qiskit.utils import algorithm_globals +from qiskit.algorithms.optimizers import SPSA +from qiskit.algorithms.minimum_eigensolvers import VQE +from qiskit.primitives import Estimator + + +def run(bond_distance: float = 2.5): + driver = PySCFDriver( + atom=f"Li 0 0 0; H 0 0 {bond_distance}", + basis="sto3g", + charge=0, + spin=0, + unit=DistanceUnit.ANGSTROM, + ) + problem = driver.run() + + active_space_trafo = ActiveSpaceTransformer( + num_electrons=problem.num_particles, num_spatial_orbitals=3 + ) + problem = active_space_trafo.transform(problem) + qubit_converter = QubitConverter(ParityMapper(), two_qubit_reduction=True) + + ansatz = EfficientSU2(num_qubits=4, reps=1, entanglement="linear", insert_barriers=True) + + np.random.seed(5) + algorithm_globals.random_seed = 5 + + + optimizer = SPSA(maxiter=100) + initial_point = np.random.random(ansatz.num_parameters) + + estimator = Estimator() + local_vqe = VQE( + estimator, + ansatz, + optimizer, + initial_point=initial_point, + ) + + local_vqe_groundstate_solver = GroundStateEigensolver(qubit_converter, local_vqe) + local_vqe_result = local_vqe_groundstate_solver.solve(problem) + + print(local_vqe_result) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--bond_length", + help="Bond length in Angstrom.", + default=2.5, + type=float, + ) + args = parser.parse_args() + + print(f"Running for bond length {args.bond_length}.") + run(args.bond_length) diff --git a/docs/getting_started/source_files/gs_level_3.py b/docs/getting_started/source_files/gs_level_3.py new file mode 100644 index 000000000..0dcec4965 --- /dev/null +++ b/docs/getting_started/source_files/gs_level_3.py @@ -0,0 +1,50 @@ +from qiskit import QuantumCircuit +from qiskit.circuit.random import random_circuit +from qiskit.quantum_info import SparsePauliOp +from qiskit.primitives import Estimator + +from quantum_serverless import QuantumServerless, run_qiskit_remote, get, put +from quantum_serverless.core.state import RedisStateHandler + +# 1. let's annotate out function to convert it +# to function that can be executed remotely +# using `run_qiskit_remote` decorator +@run_qiskit_remote() +def my_function(circuit: QuantumCircuit, obs: SparsePauliOp): + return Estimator().run([circuit], [obs]).result().values + + +# 2. Next let's create out serverless object to control +# where our remote function will be executed +serverless = QuantumServerless() + +# 2.1 (Optional) state handler to write/read results in/out of job +state_handler = RedisStateHandler("redis", 6379) + +circuits = [random_circuit(2, 2) for _ in range(3)] + +# 3. create serverless context +with serverless.context(): + # 4. let's put some shared objects into remote storage that will be shared among all executions + obs_ref = put(SparsePauliOp(["ZZ"])) + + # 4. run our function and get back reference to it + # as now our function it remote one + function_reference = my_function(circuits[0], obs_ref) + + # 4.1 or we can run N of them in parallel (for all circuits) + function_references = [my_function(circ, obs_ref) for circ in circuits] + + # 5. to get results back from reference + # we need to call `get` on function reference + single_result = get(function_reference) + parallel_result = get(function_references) + print("Single execution:", single_result) + print("N parallel executions:", parallel_result) + + # 5.1 (Optional) write results to state. + state_handler.set("result", { + "status": "ok", + "single": single_result.tolist(), + "parallel_result": [entry.tolist() for entry in parallel_result] + }) diff --git a/docs/index.rst b/docs/index.rst index 3c3b8985c..d2c07e05e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,6 +12,14 @@ The source code to the project is available `on GitHub + **Guides** .. toctree:: From 5f2ba14b937b3bcd5ba148e1f98deb8acdb93e72 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Mon, 20 Feb 2023 12:10:32 -0500 Subject: [PATCH 02/15] Issue 217 | Docs: update readme --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d37bf13bb..3d72af5e3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ # Quantum serverless +Quantum Serverless is a user-friendly tool that enables you to easily run complex quantum computing tasks. +With this software, you can execute Qiskit programs as long running jobs and distribute them across multiple CPUs, GPUs, and QPUs. +This means you can take on more complex quantum-classical programs and run them with ease. +You don't have to worry about configuration or scaling up computational resources, as Quantum Serverless takes care of everything for you. + ![diagram](./docs/images/qs_diagram.png) ### Table of Contents @@ -13,20 +18,66 @@ 1. [Installation](INSTALL.md) 2. [Quickstart](#quickstart-guide) 3. [Beginners Guide](docs/beginners_guide.md) -4. Modules: +4. [Getting started](docs/getting_started/) +5. Modules: 1. [Client](./client) 2. [Infrastructure](./infrastructure) -5. [Tutorials](docs/tutorials/) -6. [Guides](docs/guides/) -7. [How to Give Feedback](#how-to-give-feedback) -8. [Contribution Guidelines](#contribution-guidelines) -9. [References and Acknowledgements](#references-and-acknowledgements) -10. [License](#license) +6. [Tutorials](docs/tutorials/) +7. [Guides](docs/guides/) +8. [How to Give Feedback](#how-to-give-feedback) +9. [Contribution Guidelines](#contribution-guidelines) +10. [References and Acknowledgements](#references-and-acknowledgements) +11. [License](#license) ---------------------------------------------------------------------------------------------------- ### Quickstart + + +
+ 1. Hello World quickstart + +1 - Create program file `hello_qiskit.py` +```python +# hello_qiskit.py + +from qiskit import QuantumCircuit +from qiskit.primitives import Sampler + +circuit = QuantumCircuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure_all() +circuit.draw() + +sampler = Sampler() + +quasi_dists = sampler.run(circuit).result().quasi_dists + +print(f"Quasi distribution: {quasi_dists[0]}") +``` + +2 - Run program file +```python +from quantum_serverless import QuantumServerless, Program +serverless = QuantumServerless(...) # serverless setup is provided by your admin or use docker compose (refer to all-in-one quickstart) +program = Program( + name="Hello Qiskit!", + entrypoint="hello_qiskit.py", + working_dir="./" +) + +job = serverless.run_program(program) +job.logs() +# 'Quasi distribution: {0: 0.4999999999999999, 3: 0.4999999999999999}\n' +``` +
+ + +
+ 2. All-in-one quickstart + Steps 1. prepare infrastructure 2. write your program @@ -146,6 +197,9 @@ state_handler.get("result") # (Optional) get written data # 'parallel_result': [[1.0], [0.0], [-0.28650496]]} ``` +
+ +For more detailed examples and explanations refer to [Beginners Guide](docs/beginners_guide.md) and [Getting started examples](docs/getting_started/) ---------------------------------------------------------------------------------------------------- From 517ebd1a397d982a5e45a3d6a5ffc1cd32b5b30e Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Mon, 13 Mar 2023 19:10:59 -0400 Subject: [PATCH 03/15] Issue 254 | Gateway service init works --- client/quantum_serverless/__init__.py | 1 + client/quantum_serverless/core/__init__.py | 2 +- client/quantum_serverless/core/job.py | 119 +- client/quantum_serverless/core/provider.py | 209 +- .../quantum_serverless/quantum_serverless.py | 113 +- docker-compose.yml | 50 +- gateway/.gitignore | 137 + gateway/api/__init__.py | 0 gateway/api/admin.py | 17 + gateway/api/apps.py | 6 + gateway/api/migrations/0001_initial.py | 22 + gateway/api/migrations/0002_job.py | 22 + .../api/migrations/0003_program_artifact.py | 19 + .../0004_job_author_program_author.py | 28 + .../api/migrations/0005_alter_job_result.py | 18 + gateway/api/migrations/0006_job_status.py | 18 + .../api/migrations/0007_computeresource.py | 21 + .../migrations/0008_computeresource_users.py | 20 + gateway/api/migrations/0009_job_ray_job_id.py | 18 + .../migrations/0010_job_compute_resource.py | 19 + .../api/migrations/0011_alter_job_status.py | 18 + gateway/api/migrations/__init__.py | 0 gateway/api/models.py | 58 + gateway/api/permissions.py | 10 + gateway/api/serializers.py | 15 + gateway/api/tests.py | 3 + gateway/api/views.py | 123 + gateway/gateway/__init__.py | 0 gateway/gateway/asgi.py | 16 + gateway/gateway/settings.py | 177 ++ gateway/gateway/urls.py | 34 + gateway/gateway/wsgi.py | 16 + gateway/manage.py | 22 + gateway/requirements.txt | 7 + realm-export.json | 2377 +++++++++++++++++ 35 files changed, 3576 insertions(+), 159 deletions(-) create mode 100644 gateway/.gitignore create mode 100644 gateway/api/__init__.py create mode 100644 gateway/api/admin.py create mode 100644 gateway/api/apps.py create mode 100644 gateway/api/migrations/0001_initial.py create mode 100644 gateway/api/migrations/0002_job.py create mode 100644 gateway/api/migrations/0003_program_artifact.py create mode 100644 gateway/api/migrations/0004_job_author_program_author.py create mode 100644 gateway/api/migrations/0005_alter_job_result.py create mode 100644 gateway/api/migrations/0006_job_status.py create mode 100644 gateway/api/migrations/0007_computeresource.py create mode 100644 gateway/api/migrations/0008_computeresource_users.py create mode 100644 gateway/api/migrations/0009_job_ray_job_id.py create mode 100644 gateway/api/migrations/0010_job_compute_resource.py create mode 100644 gateway/api/migrations/0011_alter_job_status.py create mode 100644 gateway/api/migrations/__init__.py create mode 100644 gateway/api/models.py create mode 100644 gateway/api/permissions.py create mode 100644 gateway/api/serializers.py create mode 100644 gateway/api/tests.py create mode 100644 gateway/api/views.py create mode 100644 gateway/gateway/__init__.py create mode 100644 gateway/gateway/asgi.py create mode 100644 gateway/gateway/settings.py create mode 100644 gateway/gateway/urls.py create mode 100644 gateway/gateway/wsgi.py create mode 100755 gateway/manage.py create mode 100644 gateway/requirements.txt create mode 100644 realm-export.json diff --git a/client/quantum_serverless/__init__.py b/client/quantum_serverless/__init__.py index fa8e54c7e..71beb3cf6 100644 --- a/client/quantum_serverless/__init__.py +++ b/client/quantum_serverless/__init__.py @@ -24,6 +24,7 @@ ) from .quantum_serverless import QuantumServerless from .core.program import Program +from .core import Provider, KuberayProvider, GatewayProvider try: __version__ = metadata_version("quantum_serverless") diff --git a/client/quantum_serverless/core/__init__.py b/client/quantum_serverless/core/__init__.py index c4fddc01d..465b76ba5 100644 --- a/client/quantum_serverless/core/__init__.py +++ b/client/quantum_serverless/core/__init__.py @@ -55,7 +55,7 @@ RedisStateHandler """ -from .provider import Provider, ComputeResource, KuberayProvider +from .provider import Provider, ComputeResource, KuberayProvider, GatewayProvider from .decorators import remote, get, put, run_qiskit_remote, get_refs_by_status from .events import RedisEventHandler, EventHandler, ExecutionMessage from .state import RedisStateHandler, StateHandler diff --git a/client/quantum_serverless/core/job.py b/client/quantum_serverless/core/job.py index 0d8a1c1ca..7bebf5d25 100644 --- a/client/quantum_serverless/core/job.py +++ b/client/quantum_serverless/core/job.py @@ -27,19 +27,120 @@ RuntimeEnv Job """ - +import json +import logging from typing import Iterator +from uuid import uuid4 import ray.runtime_env +import requests from ray.dashboard.modules.job.sdk import JobSubmissionClient +from quantum_serverless.core.program import Program +from quantum_serverless.core.constants import OT_PROGRAM_NAME + RuntimeEnv = ray.runtime_env.RuntimeEnv +class BaseJobClient: + def run_program(self, program: Program) -> 'Job': + raise NotImplementedError + + def status(self, job_id: str): + raise NotImplementedError + + def stop(self, job_id: str): + raise NotImplementedError + + def logs(self, job_id: str): + raise NotImplementedError + + def result(self, job_id: str): + raise NotImplementedError + + +class RayJobClient(BaseJobClient): + def __init__(self, client: JobSubmissionClient): + self._job_client = client + + def status(self, job_id: str): + return self._job_client.get_job_status(job_id) + + def stop(self, job_id: str): + return self._job_client.stop_job(job_id) + + def logs(self, job_id: str): + return self._job_client.get_job_logs(job_id) + + def result(self, job_id: str): + return self.logs(job_id) + + def run_program(self, program: Program): + arguments = "" + if program.arguments is not None: + arg_list = [] + for key, value in program.arguments.items(): + if isinstance(value, dict): + arg_list.append(f"--{key}='{json.dumps(value)}'") + else: + arg_list.append(f"--{key}={value}") + arguments = " ".join(arg_list) + entrypoint = f"python {program.entrypoint} {arguments}" + + # set program name so OT can use it as parent span name + env_vars = {**(program.env_vars or {}), **{OT_PROGRAM_NAME: program.name}} + + job_id = self._job_client.submit_job( + entrypoint=entrypoint, + submission_id=f"qs_{uuid4()}", + runtime_env={ + "working_dir": program.working_dir, + "pip": program.dependencies, + "env_vars": env_vars, + }, + ) + return Job(job_id=job_id, job_client=self) + + +class GatewayJobClient(BaseJobClient): + def __init__(self, host: str, token: str): + self.host = host + self._token = token + + def status(self, job_id: str): + default_status = "Unknown" + status = default_status + response = requests.get(f"{self.host}/jobs/{job_id}/", headers={ + 'Authorization': f'Bearer {self._token}' + }) + if response.ok: + status = json.loads(response.text).get("status", default_status) + else: + logging.warning(f"Something went wrong during job status fetching. {response.text}") + return status + + def stop(self, job_id: str): + raise NotImplementedError + + def logs(self, job_id: str): + raise NotImplementedError + + def result(self, job_id: str): + result = None + response = requests.get(f"{self.host}/jobs/{job_id}/", headers={ + 'Authorization': f'Bearer {self._token}' + }) + if response.ok: + result = json.loads(response.text).get("result", None) + else: + logging.warning(f"Something went wrong during job result fetching. {response.text}") + return result + + class Job: """Job.""" - def __init__(self, job_id: str, job_client: JobSubmissionClient): + def __init__(self, job_id: str, job_client: BaseJobClient): """Job class for async script execution. Args: @@ -51,19 +152,19 @@ def __init__(self, job_id: str, job_client: JobSubmissionClient): def status(self): """Returns status of the job.""" - return self._job_client.get_job_status(self.job_id) + return self._job_client.status(self.job_id) def stop(self): """Stops the job from running.""" - return self._job_client.stop_job(self.job_id) + return self._job_client.stop(self.job_id) def logs(self) -> str: """Returns logs of the job.""" - return self._job_client.get_job_logs(self.job_id) + return self._job_client.logs(self.job_id) - def logs_iterator(self) -> Iterator[str]: - """Returns logs iterator.""" - return self._job_client.tail_job_logs(self.job_id) + def result(self): + """Return results of the job.""" + return self._job_client.result(self.job_id) def __repr__(self): - return f"" + return f"" diff --git a/client/quantum_serverless/core/provider.py b/client/quantum_serverless/core/provider.py index 6993dc774..2fe858077 100644 --- a/client/quantum_serverless/core/provider.py +++ b/client/quantum_serverless/core/provider.py @@ -28,6 +28,9 @@ """ import json import logging +import os.path +import tarfile +from abc import abstractmethod from dataclasses import dataclass from typing import Optional, List, Dict from uuid import uuid4 @@ -39,7 +42,7 @@ from quantum_serverless.core.constants import OT_PROGRAM_NAME, RAY_IMAGE from quantum_serverless.core.tracing import _trace_env_vars -from quantum_serverless.core.job import Job +from quantum_serverless.core.job import Job, RayJobClient, GatewayJobClient, BaseJobClient from quantum_serverless.core.program import Program from quantum_serverless.exception import QuantumServerlessException from quantum_serverless.utils import JsonSerializable @@ -66,7 +69,7 @@ class ComputeResource: port_job_server: int = 8265 resources: Optional[Dict[str, float]] = None - def job_client(self) -> Optional[JobSubmissionClient]: + def job_client(self) -> Optional[BaseJobClient]: """Return job client for given compute resource. Returns: @@ -83,7 +86,7 @@ def job_client(self) -> Optional[JobSubmissionClient]: "You will not be able to run jobs on this provider.", connection_url, ) - return client + return RayJobClient(client) return None def context(self, **kwargs): @@ -134,12 +137,12 @@ class Provider(JsonSerializable): """Provider.""" def __init__( - self, - name: str, - host: Optional[str] = None, - token: Optional[str] = None, - compute_resource: Optional[ComputeResource] = None, - available_compute_resources: Optional[List[ComputeResource]] = None, + self, + name: str, + host: Optional[str] = None, + token: Optional[str] = None, + compute_resource: Optional[ComputeResource] = None, + available_compute_resources: Optional[List[ComputeResource]] = None, ): """Provider for serverless computation. @@ -213,6 +216,33 @@ def delete_compute_resource(self, resource) -> int: """Delete compute resource for provider.""" raise NotImplementedError + def get_jobs(self, **kwargs) -> List[Job]: + """Return list of jobs. + + Returns: + list of jobs. + """ + raise NotImplementedError + + def get_job_by_id(self, job_id: str) -> Optional[Job]: + """Returns job by job id. + + Args: + job_id: job id + + Returns: + Job instance + """ + job_client = self.job_client() + + if job_client is None: + logging.warning( # pylint: disable=logging-fstring-interpolation + "Job has not been found as no provider " + "with remote host has been configured. " + ) + return None + return Job(job_id=job_id, job_client=job_client) + def run_program(self, program: Program) -> Job: """Execute program as a async job. @@ -242,44 +272,22 @@ def run_program(self, program: Program) -> Job: ) return None - arguments = "" - if program.arguments is not None: - arg_list = [] - for key, value in program.arguments.items(): - if isinstance(value, dict): - arg_list.append(f"--{key}='{json.dumps(value)}'") - else: - arg_list.append(f"--{key}={value}") - arguments = " ".join(arg_list) - entrypoint = f"python {program.entrypoint} {arguments}" - - # set program name so OT can use it as parent span name - env_vars = {**(program.env_vars or {}), **{OT_PROGRAM_NAME: program.name}} - - job_id = job_client.submit_job( - entrypoint=entrypoint, - submission_id=f"qs_{uuid4()}", - runtime_env={ - "working_dir": program.working_dir, - "pip": program.dependencies, - "env_vars": env_vars, - }, - ) - return Job(job_id=job_id, job_client=job_client) + return job_client.run_program(program) + class KuberayProvider(Provider): """Implements CRUD for Kuberay API server.""" def __init__( - self, - name: str, - host: Optional[str] = None, - namespace: Optional[str] = "default", - img: Optional[str] = RAY_IMAGE, - token: Optional[str] = None, - compute_resource: Optional[ComputeResource] = None, - available_compute_resources: Optional[List[ComputeResource]] = None, + self, + name: str, + host: Optional[str] = None, + namespace: Optional[str] = "default", + img: Optional[str] = RAY_IMAGE, + token: Optional[str] = None, + compute_resource: Optional[ComputeResource] = None, + available_compute_resources: Optional[List[ComputeResource]] = None, ): """Kuberay provider for serverless computation. @@ -449,3 +457,122 @@ def delete_kuberay_template(self, resource): self.api_root + f"/compute_templates/{resource}", timeout=TIMEOUT, ) + + def get_jobs(self, **kwargs) -> List[Job]: + raise NotImplementedError + + +class GatewayProvider(Provider): + def __init__(self, + name: str, + host: str, + username: str, + password: str, + auth_host: Optional[str] = None + ): + super().__init__(name) + self.host = host + self.auth_host = auth_host or host + self._username = username + self._password = password + self._token = None + self._fetch_token() + + def get_compute_resources(self) -> List[ComputeResource]: + raise NotImplementedError("GatewayProvider does not support resources api yet.") + + def create_compute_resource(self, resource) -> int: + raise NotImplementedError("GatewayProvider does not support resources api yet.") + + def delete_compute_resource(self, resource) -> int: + raise NotImplementedError("GatewayProvider does not support resources api yet.") + + def get_job_by_id(self, job_id: str) -> Optional[Job]: + job = None + url = f"{self.host}/jobs/{job_id}/" + response = requests.get(url, headers={ + 'Authorization': f'Bearer {self._token}' + }) + if response.ok: + data = json.loads(response.text) + job = Job(job_id=data.get("id"), job_client=GatewayJobClient(self.host, self._token)) + else: + logging.warning(response.text) + + return job + + def run_program(self, program: Program) -> Job: + url = f"{self.host}/programs/run_program/" + file_name = os.path.join(program.working_dir, "artifact.tar") + with tarfile.open(file_name, "w") as file: + file.add(program.working_dir) + + with open(file_name, "rb") as file: + response = requests.post( + url=url, + data={ + "title": program.name, + "entrypoint": program.entrypoint + }, + files={ + "artifact": file + }, + headers={ + 'Authorization': f'Bearer {self._token}' + } + ) + if not response.ok: + raise QuantumServerlessException(f"Something went wrong with program execution. {response.text}") + + json_response = json.loads(response.text) + job_id = json_response.get("id") + + # TODO: remove file + + return Job(job_id, job_client=GatewayJobClient(self.host, self._token)) + + def get_jobs(self, **kwargs) -> List[Job]: + jobs = [] + url = f"{self.host}/jobs/" + response = requests.get(url, headers={ + 'Authorization': f'Bearer {self._token}' + }) + if response.ok: + jobs = [ + Job(job_id=job.get("id"), job_client=GatewayJobClient(self.host, self._token)) + for job in json.loads(response.text).get("results", []) + ] + else: + logging.warning(response.text) + + return jobs + + def _fetch_token(self): + realm = "Test" # TODO: get realm + client_id = "newone" # TODO: get client id + keycloak_response = requests.post( + url=f"{self.auth_host}/auth/realms/{realm}/protocol/openid-connect/token", + data={ + "username": self._username, + "password": self._password, + "client_id": client_id, + "grant_type": "password" + } + ) + if not keycloak_response.ok: + raise QuantumServerlessException("Incorrect credentials.") + + keycloak_token = json.loads(keycloak_response.text).get("access_token") + + gateway_response = requests.post( + url=f"{self.host}/dj-rest-auth/keycloak/", + data={ + "access_token": keycloak_token + } + ) + + if not gateway_response.ok: + raise QuantumServerlessException("Incorrect access token.") + + gateway_token = json.loads(gateway_response.text).get("access_token") + self._token = gateway_token diff --git a/client/quantum_serverless/quantum_serverless.py b/client/quantum_serverless/quantum_serverless.py index 5b2eae4ff..cd3c7d86d 100644 --- a/client/quantum_serverless/quantum_serverless.py +++ b/client/quantum_serverless/quantum_serverless.py @@ -46,55 +46,18 @@ class QuantumServerless: """QuantumServerless class.""" - @classmethod - def load_configuration(cls, path: str) -> "QuantumServerless": - """Creates instance from configuration file. - - Example: - >>> quantum_serverless = QuantumServerless.load_configuration("./my_config.json") - - Args: - path: path to file with configuration - - Returns: - Instance of QuantumServerless - """ - with open(path, "r") as config_file: # pylint: disable=unspecified-encoding - config = json.load(config_file) - return QuantumServerless(config) - - def __init__(self, config: Optional[Dict[str, Any]] = None): + def __init__(self, providers: Union[Provider, List[Provider]]): """Quantum serverless management class. - Example: - >>> configuration = { - >>> "providers": [{ - >>> "name": "my_provider", - >>> "compute_resource": { - >>> "name": "my_resource", - >>> "host": "", - >>> "port": 10001 - >>> } - >>> }] - >>> } - >>> quantum_serverless = QuantumServerless(configuration) - >>> - >>> with quantum_serverless.provider("my_provider"): - >>> ... - - Example: - >>> quantum_serverless = QuantumServerless() - >>> - >>> with quantum_serverless: - >>> ... - Args: config: configuration Raises: QuantumServerlessException """ - self._providers: List[Provider] = load_config(config) + if isinstance(providers, Provider): + providers = [providers] + self._providers: List[Provider] = providers self._selected_provider: Provider = self._providers[-1] self._allocated_context: Optional[Context] = None @@ -140,75 +103,27 @@ def run_program(self, program: Program) -> Optional[Job]: """ return self._selected_provider.run_program(program) - def run_job( - self, - entrypoint: str, - runtime_env: Optional[Union[Dict[str, Any], RuntimeEnv]] = None, - ) -> Optional[Job]: - """Runs given entrypoint script as job. - - Example: - >>> job = QuantumServerless(...).run_job( - >>> entrypoint="python job.py", - >>> runtime_env={ - >>> "working_dir": "./", - >>> # "pip": ["requests==2.26.0"] - >>> } - >>> ) - >>> job.status() - >>> job.logs() + def get_job_by_id(self, job_id: str) -> Optional[Job]: + """Returns job by job id. Args: - entrypoint: how to execute your job - runtime_env: workdir, extra dependencies, etc. + job_id: job id Returns: - job + Job instance """ - warnings.warn( - "Function run_job is deprecated and will be removed in future releases." - "Please, use run_program instead.", - DeprecationWarning, - stacklevel=2, - ) - - job_client = self.job_client + return self._selected_provider.get_job_by_id(job_id) - if job_client is None: - logging.warning( # pylint: disable=logging-fstring-interpolation - f"Job has not been submitted as no provider " - f"with remote host has been configured. " - f"Selected provider: {self._selected_provider}" - ) - return None - - job_id = job_client.submit_job( - entrypoint=entrypoint, - submission_id=f"qs_{uuid4()}", - runtime_env=runtime_env, - ) - return Job(job_id=job_id, job_client=job_client) - - def get_job_by_id(self, job_id: str) -> Optional[Job]: - """Returns job by job id. + def get_jobs(self, **kwargs): + """Return jobs. Args: - job_id: job id + **kwargs: filters Returns: - Job instance + list of jobs """ - job_client = self.job_client - - if job_client is None: - logging.warning( # pylint: disable=logging-fstring-interpolation - f"Job has not been found as no provider " - f"with remote host has been configured. " - f"Selected provider: {self._selected_provider}" - ) - return None - job_client.get_job_info(job_id) - return Job(job_id=job_id, job_client=job_client) + return self._selected_provider.get_jobs(**kwargs) def context( self, diff --git a/docker-compose.yml b/docker-compose.yml index b908de537..6b75bcab7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,12 @@ # compose config on nightly builds services: - jupyter: - container_name: qs-jupyter - image: qiskit/quantum-serverless-notebook:nightly-py39 - ports: - - 8888:8888 - networks: - - safe-tier +# jupyter: +# container_name: qs-jupyter +# image: qiskit/quantum-serverless-notebook:nightly-py39 +# ports: +# - 8888:8888 +# networks: +# - safe-tier ray-head: container_name: ray-head image: qiskit/quantum-serverless-ray-node:nightly-py39 @@ -18,8 +18,8 @@ services: - OT_JAEGER_HOST_KEY=jaeger ports: - 8265:8265 - - 8000:8000 - - 10001:10001 +# - 8000:8000 +# - 10001:10001 privileged: true volumes: - /dev/shm:/dev/shm @@ -49,5 +49,37 @@ services: - 9411:9411 networks: - safe-tier + postgres: + image: postgres + environment: + POSTGRES_DB: testkeycloakdb + POSTGRES_USER: testkeycloakuser + POSTGRES_PASSWORD: testkeycloakpassword + restart: + always + + keycloak: + image: jboss/keycloak:16.1.0 + volumes: + - ./realm-export.json:/opt/jboss/keycloak/imports/realm-export.json + command: + - "-b 0.0.0.0 -Dkeycloak.profile.feature.upload_scripts=enabled -Dkeycloak.import=/opt/jboss/keycloak/imports/realm-export.json" + environment: + DB_VENDOR: POSTGRES + DB_ADDR: postgres + DB_DATABASE: testkeycloakdb + DB_USER: testkeycloakuser + DB_SCHEMA: public + DB_PASSWORD: testkeycloakpassword + KEYCLOAK_USER: keycloakuser + KEYCLOAK_PASSWORD: keycloakpassword + PROXY_ADDRESS_FORWARDING: "true" + KEYCLOAK_LOGLEVEL: DEBUG + ports: + - '8085:8080' + depends_on: + - postgres + restart: + always networks: safe-tier: diff --git a/gateway/.gitignore b/gateway/.gitignore new file mode 100644 index 000000000..ca6c01076 --- /dev/null +++ b/gateway/.gitignore @@ -0,0 +1,137 @@ +# Django # +*.log +*.pot +*.pyc +__pycache__ +db.sqlite3 +media + +# Backup files # +*.bak + +# If you are using PyCharm # +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# File-based project format +*.iws + +# IntelliJ +out/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Python # +*.py[cod] +*$py.class + +# Distribution / packaging +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Sublime Text # +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files Package +Control.last-run +Control.ca-list +Control.ca-bundle +Control.system-ca-bundle +GitHub.sublime-settings + +# Visual Studio Code # +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history diff --git a/gateway/api/__init__.py b/gateway/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gateway/api/admin.py b/gateway/api/admin.py new file mode 100644 index 000000000..e264716b7 --- /dev/null +++ b/gateway/api/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from .models import Job, Program, ComputeResource + + +@admin.register(Program) +class ProgramAdmin(admin.ModelAdmin): + pass + + +@admin.register(Job) +class JobAdmin(admin.ModelAdmin): + pass + + +@admin.register(ComputeResource) +class ComputeResourceAdmin(admin.ModelAdmin): + pass diff --git a/gateway/api/apps.py b/gateway/api/apps.py new file mode 100644 index 000000000..66656fd29 --- /dev/null +++ b/gateway/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/gateway/api/migrations/0001_initial.py b/gateway/api/migrations/0001_initial.py new file mode 100644 index 000000000..25508a3cc --- /dev/null +++ b/gateway/api/migrations/0001_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1 on 2023-03-13 14:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Program', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('entrypoint', models.CharField(max_length=255)), + ], + ), + ] diff --git a/gateway/api/migrations/0002_job.py b/gateway/api/migrations/0002_job.py new file mode 100644 index 000000000..ac15fe51e --- /dev/null +++ b/gateway/api/migrations/0002_job.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1 on 2023-03-13 16:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Job', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('result', models.TextField()), + ('program', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.program')), + ], + ), + ] diff --git a/gateway/api/migrations/0003_program_artifact.py b/gateway/api/migrations/0003_program_artifact.py new file mode 100644 index 000000000..7785a3cb1 --- /dev/null +++ b/gateway/api/migrations/0003_program_artifact.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1 on 2023-03-13 16:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_job'), + ] + + operations = [ + migrations.AddField( + model_name='program', + name='artifact', + field=models.FileField(default='default', upload_to='artifacts_%Y_%m_%d'), + preserve_default=False, + ), + ] diff --git a/gateway/api/migrations/0004_job_author_program_author.py b/gateway/api/migrations/0004_job_author_program_author.py new file mode 100644 index 000000000..41c0e21b6 --- /dev/null +++ b/gateway/api/migrations/0004_job_author_program_author.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1 on 2023-03-13 16:28 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('api', '0003_program_artifact'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='author', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + migrations.AddField( + model_name='program', + name='author', + field=models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/gateway/api/migrations/0005_alter_job_result.py b/gateway/api/migrations/0005_alter_job_result.py new file mode 100644 index 000000000..548978116 --- /dev/null +++ b/gateway/api/migrations/0005_alter_job_result.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2023-03-13 17:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_job_author_program_author'), + ] + + operations = [ + migrations.AlterField( + model_name='job', + name='result', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/gateway/api/migrations/0006_job_status.py b/gateway/api/migrations/0006_job_status.py new file mode 100644 index 000000000..3e8bc522a --- /dev/null +++ b/gateway/api/migrations/0006_job_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2023-03-13 17:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0005_alter_job_result'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('RUNNING', 'Running'), ('ERROR', 'Error'), ('FINISHED', 'Finished')], default='PENDING', max_length=10), + ), + ] diff --git a/gateway/api/migrations/0007_computeresource.py b/gateway/api/migrations/0007_computeresource.py new file mode 100644 index 000000000..70766703e --- /dev/null +++ b/gateway/api/migrations/0007_computeresource.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1 on 2023-03-13 20:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0006_job_status'), + ] + + operations = [ + migrations.CreateModel( + name='ComputeResource', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100)), + ('host', models.CharField(max_length=100)), + ], + ), + ] diff --git a/gateway/api/migrations/0008_computeresource_users.py b/gateway/api/migrations/0008_computeresource_users.py new file mode 100644 index 000000000..26b463caa --- /dev/null +++ b/gateway/api/migrations/0008_computeresource_users.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1 on 2023-03-13 20:21 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('api', '0007_computeresource'), + ] + + operations = [ + migrations.AddField( + model_name='computeresource', + name='users', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/gateway/api/migrations/0009_job_ray_job_id.py b/gateway/api/migrations/0009_job_ray_job_id.py new file mode 100644 index 000000000..b4edbd055 --- /dev/null +++ b/gateway/api/migrations/0009_job_ray_job_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2023-03-13 20:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0008_computeresource_users'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='ray_job_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/gateway/api/migrations/0010_job_compute_resource.py b/gateway/api/migrations/0010_job_compute_resource.py new file mode 100644 index 000000000..e6883a8cd --- /dev/null +++ b/gateway/api/migrations/0010_job_compute_resource.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1 on 2023-03-13 21:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0009_job_ray_job_id'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='compute_resource', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.computeresource'), + ), + ] diff --git a/gateway/api/migrations/0011_alter_job_status.py b/gateway/api/migrations/0011_alter_job_status.py new file mode 100644 index 000000000..dd24b5563 --- /dev/null +++ b/gateway/api/migrations/0011_alter_job_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2023-03-13 21:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0010_job_compute_resource'), + ] + + operations = [ + migrations.AlterField( + model_name='job', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('RUNNING', 'Running'), ('STOPPED', 'Stopped'), ('SUCCEEDED', 'Succeeded'), ('FAILED', 'Failed')], default='PENDING', max_length=10), + ), + ] diff --git a/gateway/api/migrations/__init__.py b/gateway/api/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gateway/api/models.py b/gateway/api/models.py new file mode 100644 index 000000000..8d542d208 --- /dev/null +++ b/gateway/api/models.py @@ -0,0 +1,58 @@ +from django.db import models +from django.conf import settings + + +class Program(models.Model): + title = models.CharField(max_length=255) + entrypoint = models.CharField(max_length=255) + artifact = models.FileField(upload_to="artifacts_%Y_%m_%d", null=False, blank=False) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ) + + def __str__(self): + return f"{self.title}" + + +# TODO: create command to create default cluster +class ComputeResource(models.Model): + title = models.CharField(max_length=100, blank=False, null=False) + host = models.CharField(max_length=100, blank=False, null=False) + + users = models.ManyToManyField(settings.AUTH_USER_MODEL) + + def __str__(self): + return self.title + + +class Job(models.Model): + PENDING = "PENDING" + RUNNING = "RUNNING" + STOPPED = "STOPPED" + SUCCEEDED = "SUCCEEDED" + FAILED = "FAILED" + JOB_STATUSES = [ + (PENDING, 'Pending'), + (RUNNING, 'Running'), + (STOPPED, 'Stopped'), + (SUCCEEDED, 'Succeeded'), + (FAILED, 'Failed') + ] + + program = models.ForeignKey(to=Program, on_delete=models.SET_NULL, null=True) + result = models.TextField(null=True, blank=True) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ) + status = models.CharField( + max_length=10, + choices=JOB_STATUSES, + default=PENDING, + ) + compute_resource = models.ForeignKey(ComputeResource, on_delete=models.SET_NULL, null=True, blank=True) + ray_job_id = models.CharField(max_length=255, null=True, blank=True) + + def __str__(self): + return f"Job <{self.pk}> {self.program}" diff --git a/gateway/api/permissions.py b/gateway/api/permissions.py new file mode 100644 index 000000000..6552f10de --- /dev/null +++ b/gateway/api/permissions.py @@ -0,0 +1,10 @@ +from rest_framework import permissions + + +class IsOwner(permissions.BasePermission): + """ + Custom permission to only allow owners of an object to edit it. + """ + + def has_object_permission(self, request, view, obj): + return obj.author == request.user diff --git a/gateway/api/serializers.py b/gateway/api/serializers.py new file mode 100644 index 000000000..2caa750a1 --- /dev/null +++ b/gateway/api/serializers.py @@ -0,0 +1,15 @@ +from rest_framework import serializers + +from api.models import Program, Job + + +class ProgramSerializer(serializers.ModelSerializer): + class Meta: + model = Program + fields = ["title", "entrypoint", "artifact"] + + +class JobSerializer(serializers.ModelSerializer): + class Meta: + model = Job + fields = ["id", "result", "status"] diff --git a/gateway/api/tests.py b/gateway/api/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/gateway/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/gateway/api/views.py b/gateway/api/views.py new file mode 100644 index 000000000..e0d1bbc81 --- /dev/null +++ b/gateway/api/views.py @@ -0,0 +1,123 @@ +import json +import os.path +import shutil +import tarfile +import uuid + +from django.conf import settings +from ray.dashboard.modules.job.common import JobStatus +from ray.dashboard.modules.job.sdk import JobSubmissionClient +from rest_framework import viewsets, permissions, status +from allauth.socialaccount.providers.keycloak.views import KeycloakOAuth2Adapter +from dj_rest_auth.registration.views import SocialLoginView +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response + +from .models import Program, Job, ComputeResource +from .permissions import IsOwner +from .serializers import ProgramSerializer, JobSerializer + + +class ProgramViewSet(viewsets.ModelViewSet): + queryset = Program.objects.all() + serializer_class = ProgramSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Program.objects.all().filter(author=self.request.user) + + def perform_create(self, serializer): + serializer.save(author=self.request.user) + + @action(methods=["POST"], detail=False) + def run_program(self, request): + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + # create program + program = Program(**serializer.data) + + existing_programs = Program.objects.filter(author=request.user, title__exact=program.title) + if existing_programs.count() > 0: + # take existing one + program = existing_programs.first() + program.artifact = request.FILES.get("artifact") + # TODO: check format of file or create serializer + program.author = request.user + program.save() + + # get available compute resources + resources = ComputeResource.objects.filter(users__in=[request.user]) + if resources.count() == 0: + return Response( + {"error": "user do not have any resources in account"}, + status=status.HTTP_400_BAD_REQUEST + ) + compute_resource = resources.first() + + # start job + ray_client = JobSubmissionClient(compute_resource.host) + # unpack data + with tarfile.open(program.artifact.path) as file: + extract_folder = os.path.join(settings.MEDIA_ROOT, "tmp", str(uuid.uuid4())) + file.extractall(extract_folder) + ray_job_id = ray_client.submit_job( + entrypoint=f"python {program.entrypoint}", + runtime_env={ + "working_dir": extract_folder + } + ) + # remote temp data + if os.path.exists(extract_folder): + shutil.rmtree(extract_folder) + + job = Job(program=program, author=request.user, ray_job_id=ray_job_id, compute_resource=compute_resource) + job.save() + return Response(JobSerializer(job).data) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class JobViewSet(viewsets.ModelViewSet): + queryset = Job.objects.all() + serializer_class = JobSerializer + permission_classes = [permissions.IsAuthenticated, IsOwner] + + def get_queryset(self): + return Job.objects.all().filter(author=self.request.user) + + def perform_create(self, serializer): + serializer.save(author=self.request.user) + + def retrieve(self, request, pk=None): + queryset = Job.objects.all() + job: Job = get_object_or_404(queryset, pk=pk) + serializer = JobSerializer(job) + if job.compute_resource: + ray_client = JobSubmissionClient(job.compute_resource.host) + ray_job_status = ray_client.get_job_status(job.ray_job_id) + job.status = ray_job_status_to_model_job_status(ray_job_status) + job.save() + return Response(serializer.data) + + @action(methods=["POST"], detail=True, permission_classes=[permissions.AllowAny]) + def result(self, request, pk=None): + job = self.get_object() + job.result = json.dumps(request.data.get("result")) + # job.status = Job.SUCCEEDED + job.save() + return Response(JobSerializer(job).data) + + +def ray_job_status_to_model_job_status(ray_job_status): + mapping = { + JobStatus.PENDING: Job.PENDING, + JobStatus.RUNNING: Job.RUNNING, + JobStatus.STOPPED: Job.STOPPED, + JobStatus.SUCCEEDED: Job.SUCCEEDED, + JobStatus.FAILED: Job.FAILED, + } + return mapping.get(ray_job_status, Job.FAILED) + +class KeycloakLogin(SocialLoginView): + adapter_class = KeycloakOAuth2Adapter diff --git a/gateway/gateway/__init__.py b/gateway/gateway/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gateway/gateway/asgi.py b/gateway/gateway/asgi.py new file mode 100644 index 000000000..59adf7671 --- /dev/null +++ b/gateway/gateway/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for gateway project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gateway.settings") + +application = get_asgi_application() diff --git a/gateway/gateway/settings.py b/gateway/gateway/settings.py new file mode 100644 index 000000000..a5e3bf421 --- /dev/null +++ b/gateway/gateway/settings.py @@ -0,0 +1,177 @@ +""" +Django settings for gateway project. + +Generated by 'django-admin startproject' using Django 4.1. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" +import os.path +from datetime import timedelta +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-&)i3b5aue*#-i6k9i-03qm(d!0h&662lbhj12on_*gimn3x8p7" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + 'django.contrib.sites', + "rest_framework", + 'rest_framework.authtoken', + 'rest_framework_simplejwt', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.keycloak', + 'dj_rest_auth', + 'dj_rest_auth.registration', + "api" +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "gateway.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / 'templates'] + , + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "gateway.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, +] + +AUTHENTICATION_BACKENDS = [ + # Needed to login by username in Django admin, regardless of `allauth` + 'django.contrib.auth.backends.ModelBackend', + + # `allauth` specific authentication methods, such as login by e-mail + 'allauth.account.auth_backends.AuthenticationBackend', +] + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', + ), + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': 100 +} + +REST_AUTH = { + 'USE_JWT': True, + # 'JWT_AUTH_COOKIE': 'gateway-app-auth', + # 'JWT_AUTH_REFRESH_COOKIE': 'gateway-refresh-token', +} + +SITE_ID = 1 + +# Provider specific settings +SETTING_KEYCLOAK_URL = "SETTING_KEYCLOAK_URL" +SETTING_KEYCLOAK_REALM = "SETTING_KEYCLOAK_REALM" +SOCIALACCOUNT_PROVIDERS = { + 'keycloak': { # TODO: via env vars + 'KEYCLOAK_URL': os.environ.get(SETTING_KEYCLOAK_URL, 'http://localhost:8085/auth'), + 'KEYCLOAK_REALM': os.environ.get(SETTING_KEYCLOAK_REALM, 'Test') + } +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=10), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=20) +} + +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_URL = '/media/' diff --git a/gateway/gateway/urls.py b/gateway/gateway/urls.py new file mode 100644 index 000000000..0a5971bc2 --- /dev/null +++ b/gateway/gateway/urls.py @@ -0,0 +1,34 @@ +"""gateway URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from rest_framework import routers + +from api.views import ProgramViewSet, KeycloakLogin, JobViewSet + +router = routers.DefaultRouter() +router.register(r'programs', ProgramViewSet) +router.register(r'jobs', JobViewSet) + + +urlpatterns = [ + path('', include(router.urls)), + path('dj-rest-auth/', include('dj_rest_auth.urls')), + path('dj-rest-auth/keycloak/', KeycloakLogin.as_view(), name='keycloak_login'), + path('accounts/', include('allauth.urls')), + path('api-auth/', include('rest_framework.urls')), + path("admin/", admin.site.urls), +] diff --git a/gateway/gateway/wsgi.py b/gateway/gateway/wsgi.py new file mode 100644 index 000000000..625660a82 --- /dev/null +++ b/gateway/gateway/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for gateway project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gateway.settings") + +application = get_wsgi_application() diff --git a/gateway/manage.py b/gateway/manage.py new file mode 100755 index 000000000..45e992b19 --- /dev/null +++ b/gateway/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gateway.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/gateway/requirements.txt b/gateway/requirements.txt new file mode 100644 index 000000000..73f482295 --- /dev/null +++ b/gateway/requirements.txt @@ -0,0 +1,7 @@ +django-filter==22.1 +djangorestframework==3.14.0 +Markdown==3.4.1 +django-allauth==0.52.0 +dj-rest-auth==3.0.0 +djangorestframework-simplejwt==5.2.2 +ray[default]==2.3.0 diff --git a/realm-export.json b/realm-export.json new file mode 100644 index 000000000..d2036ac4f --- /dev/null +++ b/realm-export.json @@ -0,0 +1,2377 @@ +{ + "id": "Test", + "realm": "Test", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "none", + "registrationAllowed": true, + "registrationEmailAsUsername": true, + "rememberMe": true, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": true, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "4833bad1-0ba1-4115-a2e1-3b96a90fe268", + "name": "default-roles-test", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ] + }, + "clientRole": false, + "containerId": "Test", + "attributes": {} + }, + { + "id": "556c8a27-7a33-4ea5-9232-525239ff6807", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "Test", + "attributes": {} + }, + { + "id": "0ad4b9c1-aad6-4b13-ae28-7ff0cb447dc0", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "Test", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "5af1d2f0-30bb-4483-bab5-210e061e2f1d", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "68d8756c-1b28-4dd0-9bde-1255b8f753c4", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "7b54160f-01d4-43d8-8bd3-101bd491a28e", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "38bcccc5-64f6-475a-a875-8fe6d8d12f95", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "702884ce-65f4-4899-97b3-1ff3b585039f", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "07f8bd0b-e340-42b3-91c8-d1a2b5bd88c5", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "8c9d0de4-a7dc-43e9-a601-e3e57cb114df", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "view-realm", + "manage-identity-providers", + "create-client", + "view-users", + "view-identity-providers", + "manage-realm", + "manage-authorization", + "query-realms", + "manage-users", + "query-groups", + "impersonation", + "manage-clients", + "manage-events", + "query-users", + "view-authorization", + "view-clients", + "view-events", + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "21f3ed07-221a-4d2c-a3ca-8df78cca9f76", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "69e415af-8434-4c00-838f-a608a6123e20", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "08a45a5b-d94a-486f-a021-75b7c85ecb67", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "a620f375-f013-4474-adb8-a8749d0fe039", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "02aaa954-c09e-41a9-9933-aed44514d94e", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "a1f26bd1-6703-4b88-9d17-eed35ba03fd5", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "eebbe4e0-0079-4f9a-91ca-27994aa35d91", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "d58170d1-fee7-4eac-b2e0-5dd3a95a3433", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "1ef1a2b4-ea4f-4951-8452-60fb9148e0ed", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "e122d715-8b55-4997-b79b-ad81899f854a", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "cf138fd9-4093-4715-a4e0-edaf6bbd1e56", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "97d75e8b-ef13-4084-adc4-fa70df05d52f", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "test-client": [ + { + "id": "3f58b737-9ffa-455e-a36b-5a1b3f089080", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "9a76b2ec-b33e-40b0-9cad-e00ca7e77e40", + "attributes": {} + } + ], + "account-console": [], + "broker": [], + "account": [ + { + "id": "5d39ee7c-40a9-4656-a6f7-05efc0b00002", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "attributes": {} + }, + { + "id": "e6b688f3-4c5b-4381-96ff-9f6617a9c515", + "name": "manage-account", + "composite": false, + "clientRole": true, + "containerId": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "4833bad1-0ba1-4115-a2e1-3b96a90fe268", + "name": "default-roles-test", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "Test" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "33b940e2-0bdb-49a7-9356-e6e230f49619", + "createdTimestamp": 1640089861472, + "username": "service-account-admin-cli", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "admin-cli", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "offline_access", + "default-roles-test", + "uma_authorization" + ], + "clientRoles": { + "realm-management": [ + "manage-identity-providers", + "view-realm", + "create-client", + "view-users", + "view-identity-providers", + "manage-realm", + "realm-admin", + "manage-authorization", + "query-realms", + "impersonation", + "manage-users", + "query-groups", + "manage-clients", + "manage-events", + "query-users", + "view-authorization", + "view-clients", + "view-events", + "query-clients" + ], + "account": [ + "delete-account", + "manage-account" + ] + }, + "notBefore": 0, + "groups": [] + }, + { + "id": "83d84b8e-f053-480e-8b13-713c4fac708d", + "createdTimestamp": 1640089810342, + "username": "service-account-test-client", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "test-client", + "disableableCredentialTypes": [], + "requiredActions": [], + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "test-client": [ + { + "client": "admin-cli", + "roles": [ + "uma_protection" + ] + } + ], + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/Test/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/Test/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "207a4d3c-cc80-4bd2-91d4-815a1af38778", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/Test/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/Test/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "70d4fa1a-79b2-489e-b9a0-47a6772819a6", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "f8f4baad-a231-4a6a-b97c-5d68ac147279", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "NKlUMjdSJBcnMkJBPhQwXQQfbtJfAyme", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "oauth2.device.authorization.grant.enabled": "true", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "false", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "a73b0f3e-1b0c-4b14-893e-22f4985cfd60", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "030a393a-ff89-4d2e-aa30-063e95b7ce9f", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "8e4e8915-cba7-4be3-86e8-d6991a0cd273", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:admin-cli:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "98ea544d-9474-4cde-a7d5-f4aa8438596b", + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "id": "3747a4f9-0b6b-4ad0-aba4-181193729727", + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "id": "762a6303-aab7-439b-8a41-0973964640ce", + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:admin-cli:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "1d1a4841-fbfe-4bda-9bc8-fdc73497aa5c", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "fb6c4935-1d0c-4e82-b262-443672d72930", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "97d658fa-02d4-43d5-9bba-4d0717a8466d", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/Test/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/Test/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "fb2e09ee-c7b0-49b2-870d-758173ec6be7", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "9a76b2ec-b33e-40b0-9cad-e00ca7e77e40", + "clientId": "test-client", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "AQ3sZ4eiF7NhOtfxeUEGo0YN7uQBoUnO", + "redirectUris": [ + "http://localhost:8081/callback" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "3716053c-9672-4685-9fe5-0b44307c65c1", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "4cffb7d8-1aab-4b35-8111-df1ee341c76a", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "57540600-0bd8-42dd-8eb1-ca4177c2da57", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:test-client:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "c4c07a91-21b2-4259-b923-4b3d6b05d93f", + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "id": "b1174446-ce63-4d3d-8829-f1b960a76b42", + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "id": "c595a3a7-c4d3-47b1-896d-50e5396d1eee", + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:test-client:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + } + ], + "clientScopes": [ + { + "id": "a894dbe0-76e7-4c22-b7b2-bd3f827e0ef5", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "762589d9-35be-4ad7-bed4-4b718d6ef6ec", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "5a5ce089-2139-4d60-8d2a-fd198c5db2ec", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "927a5908-7652-4586-9b8a-eb5920ef4150", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "bf4c9750-93e5-434e-8845-adb5d545b462", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "c3557b80-20cf-41cc-9732-9ebc2bd65e8a", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "9b1e384f-9aed-4592-a40e-734030fdcfcb", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "b19ae76e-fce0-4f6b-8d84-378f60d88f8d", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "83b45cee-daa8-4a98-af4b-b9000f36f2fd", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "8695784f-2e6b-4571-982b-26b8ba72af98", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "f36f78cb-da3f-4377-8b90-7d28078cc890", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "39baba4a-03aa-4309-8cd2-2591181f21ba", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "a2a22f05-cf5a-4206-9c7d-57fba22073c9", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "20187807-6f9e-4438-abec-164ca4e39520", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ec0661bc-d266-4af6-aac4-a1753b1291d4", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "a9b3a239-bc80-4067-b787-a2c3ca0d2ec4", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "e3d6fefe-3579-47a1-807d-64fcf7a87dcf", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "e832567a-5345-4f8c-8b35-012f65396f67", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "fd7a31da-915a-40ae-b633-393615ce2762", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "75ffd8aa-4326-4923-bc3e-20b09bd875b0", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "f2654fbe-5521-49e4-8e50-ca04651db68b", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "3cf479a7-f66d-4274-af23-ed1c7909b6e5", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "ef85df6a-0b3d-400b-b882-a2118ad44db5", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "84451abc-bcbc-4451-9dcf-32836641765c", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "67318bb2-5f53-4f75-a587-8f3319ebe843", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "820f7a36-03eb-4503-aa11-5742efe7390e", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "233c42eb-c87d-4826-8bbc-4683c4f13a1a", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "eb34b957-ea38-41ac-9199-697e227985e7", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "50ba7956-d15f-4d8c-90aa-da136f09dcb2", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "3f7b3d46-c9f0-43c2-90e9-e1a4874bdbcf", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "01ac5e4b-4945-4667-be10-d29dc5e6ad47", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "cdb0ce02-86a3-4e76-84f7-167ede3e0ecf", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "04ef696e-7196-4c73-872d-10af8ebe4276", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d8c56c76-ff18-4b37-b45a-237fcf8b2950", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "d21719d1-850e-488f-a58d-a4e42c76f2a5", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "632544be-5a8c-4e7e-b3c8-4cb5faedcf66", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "3743b061-854b-43fd-8fcc-b687d015e9b5", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "7051cfe2-ab43-4faa-b40d-af6446b18167", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "e0ec37dd-5965-48d3-81a6-3cb99629ccce", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "929899ea-bf1d-42b0-bd2a-9d1e432db44f", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-address-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper" + ] + } + }, + { + "id": "d4a2ebb9-a3ae-44be-8678-3e00952c4b94", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "891f4a61-7f6e-4523-af0f-f11c55e9113c", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "bd63ffe9-c748-4d6a-85ea-4677fa6260c7", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "41f8cc61-7aeb-44b5-ad6b-990382a76fad", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "acd1a5ea-6013-4353-beb1-4b8b00f50970", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "5b2b6b08-9d27-481a-9110-92ddba95a032", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "18f53e6d-9820-4064-ab92-4b4d59766399", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "bb03bb29-3654-40bd-89cf-b97eb025fdf6", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "9b9bd673-f110-4a4e-ac11-843a66e68b3a", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "6f683a06-820d-4ed6-9515-4df32eb81b2b", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "941107ba-0473-434a-b421-002e4f1a69d5", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "ca433bce-5571-4ea7-a244-83943d7bb32a", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "4889a6d9-7042-4834-9a2c-25cfcf7ceee7", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "89683d07-86f4-4742-9427-c503aec8f5b2", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "f335ed3a-4d73-4a01-a454-e8028a75268b", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "64fe78c2-bb50-4085-8cf7-3b398dacf85f", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "f7570db5-d586-4f6d-ba46-9c01a7c75208", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d540056f-3c26-4bed-aafc-4e8b15f7a9c8", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "c68c0e86-4c68-4afb-9510-a13617149205", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "c20894af-e1c1-4003-80e0-2a1d3006b31f", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "af8caa37-58f2-42fa-a65b-0bcf98bd9e6e", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "dd6c1bd1-b8d7-40b0-ab1a-db85cc3461d8", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "3ce923e0-c377-4ccb-9f95-061aabc04bef", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "771ee177-6a6d-47d7-97e1-16bc28583d27", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d6c90c8d-5f7a-4653-81d7-188074cc2ffe", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "5aeb15de-bf71-4444-8867-62fd4d347e16", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "6192846f-4ecf-4fea-9fe6-2ce2d6cef01b", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "5bf41189-9e07-4178-b203-542b69d751a6", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "96c00f93-64c6-4e2f-b784-e03213c6582e", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "44007966-d7cc-47d3-b966-13e6278a878f", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5" + }, + "keycloakVersion": "16.1.0", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} From dc06875dd5c3898888377b60cdc3fcf44a729033 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Tue, 14 Mar 2023 15:42:17 -0400 Subject: [PATCH 04/15] Issue 254 | Client: refactor tests + linter --- client/quantum_serverless/__init__.py | 2 +- client/quantum_serverless/core/constants.py | 7 + client/quantum_serverless/core/job.py | 52 ++++-- client/quantum_serverless/core/provider.py | 152 +++++++++++------- .../quantum_serverless/quantum_serverless.py | 11 +- client/tests/core/test_job.py | 59 ------- client/tests/core/test_program.py | 24 ++- client/tests/core/test_state.py | 31 ++-- client/tests/test_quantum_serverless.py | 33 +--- gateway/requirements-dev.txt | 6 + gateway/requirements.txt | 1 + gateway/tox.ini | 35 ++++ 12 files changed, 210 insertions(+), 203 deletions(-) delete mode 100644 client/tests/core/test_job.py create mode 100644 gateway/requirements-dev.txt create mode 100644 gateway/tox.ini diff --git a/client/quantum_serverless/__init__.py b/client/quantum_serverless/__init__.py index 71beb3cf6..11f15a1cb 100644 --- a/client/quantum_serverless/__init__.py +++ b/client/quantum_serverless/__init__.py @@ -21,10 +21,10 @@ put, get_refs_by_status, KuberayProvider, + GatewayProvider, ) from .quantum_serverless import QuantumServerless from .core.program import Program -from .core import Provider, KuberayProvider, GatewayProvider try: __version__ = metadata_version("quantum_serverless") diff --git a/client/quantum_serverless/core/constants.py b/client/quantum_serverless/core/constants.py index 8b12de4bb..b2094bded 100644 --- a/client/quantum_serverless/core/constants.py +++ b/client/quantum_serverless/core/constants.py @@ -24,3 +24,10 @@ # container image RAY_IMAGE = "qiskit/quantum-serverless-ray-node:latest-py39" + +# keycloak +ENV_KEYCLOAK_REALM = "ENV_KEYCLOAK_REALM" +ENV_KEYCLOAK_CLIENT_ID = "ENV_KEYCLOAK_CLIENT_ID" + +# request timeout +REQUESTS_TIMEOUT: int = 30 diff --git a/client/quantum_serverless/core/job.py b/client/quantum_serverless/core/job.py index 7bebf5d25..7682c1f14 100644 --- a/client/quantum_serverless/core/job.py +++ b/client/quantum_serverless/core/job.py @@ -29,38 +29,52 @@ """ import json import logging -from typing import Iterator from uuid import uuid4 import ray.runtime_env import requests from ray.dashboard.modules.job.sdk import JobSubmissionClient +from quantum_serverless.core.constants import OT_PROGRAM_NAME, REQUESTS_TIMEOUT from quantum_serverless.core.program import Program -from quantum_serverless.core.constants import OT_PROGRAM_NAME RuntimeEnv = ray.runtime_env.RuntimeEnv class BaseJobClient: - def run_program(self, program: Program) -> 'Job': + """Base class for Job clients.""" + + def run_program(self, program: Program) -> "Job": + """Runs program.""" raise NotImplementedError def status(self, job_id: str): + """Check status.""" raise NotImplementedError def stop(self, job_id: str): + """Stops job/program.""" raise NotImplementedError def logs(self, job_id: str): + """Return logs.""" raise NotImplementedError def result(self, job_id: str): + """Return results.""" raise NotImplementedError class RayJobClient(BaseJobClient): + """RayJobClient.""" + def __init__(self, client: JobSubmissionClient): + """Ray job client. + Wrapper around JobSubmissionClient + + Args: + client: JobSubmissionClient + """ self._job_client = client def status(self, job_id: str): @@ -103,20 +117,32 @@ def run_program(self, program: Program): class GatewayJobClient(BaseJobClient): + """GatewayJobClient.""" + def __init__(self, host: str, token: str): + """Job client for Gateway service. + + Args: + host: gateway host + token: authorization token + """ self.host = host self._token = token def status(self, job_id: str): default_status = "Unknown" status = default_status - response = requests.get(f"{self.host}/jobs/{job_id}/", headers={ - 'Authorization': f'Bearer {self._token}' - }) + response = requests.get( + f"{self.host}/jobs/{job_id}/", + headers={"Authorization": f"Bearer {self._token}"}, + timeout=REQUESTS_TIMEOUT, + ) if response.ok: status = json.loads(response.text).get("status", default_status) else: - logging.warning(f"Something went wrong during job status fetching. {response.text}") + logging.warning( + "Something went wrong during job status fetching. %s", response.text + ) return status def stop(self, job_id: str): @@ -127,13 +153,17 @@ def logs(self, job_id: str): def result(self, job_id: str): result = None - response = requests.get(f"{self.host}/jobs/{job_id}/", headers={ - 'Authorization': f'Bearer {self._token}' - }) + response = requests.get( + f"{self.host}/jobs/{job_id}/", + headers={"Authorization": f"Bearer {self._token}"}, + timeout=REQUESTS_TIMEOUT, + ) if response.ok: result = json.loads(response.text).get("result", None) else: - logging.warning(f"Something went wrong during job result fetching. {response.text}") + logging.warning( + "Something went wrong during job result fetching. %s", response.text + ) return result diff --git a/client/quantum_serverless/core/provider.py b/client/quantum_serverless/core/provider.py index 2fe858077..d597cc749 100644 --- a/client/quantum_serverless/core/provider.py +++ b/client/quantum_serverless/core/provider.py @@ -30,20 +30,27 @@ import logging import os.path import tarfile -from abc import abstractmethod from dataclasses import dataclass from typing import Optional, List, Dict -from uuid import uuid4 -import requests import ray +import requests from ray.dashboard.modules.job.sdk import JobSubmissionClient -from quantum_serverless.core.constants import OT_PROGRAM_NAME, RAY_IMAGE - -from quantum_serverless.core.tracing import _trace_env_vars -from quantum_serverless.core.job import Job, RayJobClient, GatewayJobClient, BaseJobClient +from quantum_serverless.core.constants import ( + RAY_IMAGE, + ENV_KEYCLOAK_REALM, + ENV_KEYCLOAK_CLIENT_ID, + REQUESTS_TIMEOUT, +) +from quantum_serverless.core.job import ( + Job, + RayJobClient, + GatewayJobClient, + BaseJobClient, +) from quantum_serverless.core.program import Program +from quantum_serverless.core.tracing import _trace_env_vars from quantum_serverless.exception import QuantumServerlessException from quantum_serverless.utils import JsonSerializable @@ -79,14 +86,15 @@ def job_client(self) -> Optional[BaseJobClient]: connection_url = f"http://{self.host}:{self.port_job_server}" client = None try: - client = JobSubmissionClient(connection_url) + client = RayJobClient(JobSubmissionClient(connection_url)) except ConnectionError: logging.warning( "Failed to establish connection with jobs server at %s. " "You will not be able to run jobs on this provider.", connection_url, ) - return RayJobClient(client) + + return client return None def context(self, **kwargs): @@ -137,12 +145,12 @@ class Provider(JsonSerializable): """Provider.""" def __init__( - self, - name: str, - host: Optional[str] = None, - token: Optional[str] = None, - compute_resource: Optional[ComputeResource] = None, - available_compute_resources: Optional[List[ComputeResource]] = None, + self, + name: str, + host: Optional[str] = None, + token: Optional[str] = None, + compute_resource: Optional[ComputeResource] = None, + available_compute_resources: Optional[List[ComputeResource]] = None, ): """Provider for serverless computation. @@ -275,19 +283,18 @@ def run_program(self, program: Program) -> Job: return job_client.run_program(program) - class KuberayProvider(Provider): """Implements CRUD for Kuberay API server.""" def __init__( - self, - name: str, - host: Optional[str] = None, - namespace: Optional[str] = "default", - img: Optional[str] = RAY_IMAGE, - token: Optional[str] = None, - compute_resource: Optional[ComputeResource] = None, - available_compute_resources: Optional[List[ComputeResource]] = None, + self, + name: str, + host: Optional[str] = None, + namespace: Optional[str] = "default", + img: Optional[str] = RAY_IMAGE, + token: Optional[str] = None, + compute_resource: Optional[ComputeResource] = None, + available_compute_resources: Optional[List[ComputeResource]] = None, ): """Kuberay provider for serverless computation. @@ -463,19 +470,37 @@ def get_jobs(self, **kwargs) -> List[Job]: class GatewayProvider(Provider): - def __init__(self, - name: str, - host: str, - username: str, - password: str, - auth_host: Optional[str] = None - ): + """GatewayProvider.""" + + def __init__( + self, + name: str, + host: str, + username: str, + password: str, + auth_host: Optional[str] = None, + realm: Optional[str] = None, + client_id: Optional[str] = None, + ): + """GatewayProvider. + + Args: + name: name of provider + host: host of gateway + username: username + password: password + auth_host: host of keycloak server + realm: keycloak realm + client_id: keycloak client id + """ super().__init__(name) self.host = host self.auth_host = auth_host or host self._username = username self._password = password self._token = None + self._realm = realm or os.environ.get(ENV_KEYCLOAK_REALM, "Test") + self._client_id = client_id or os.environ.get(ENV_KEYCLOAK_CLIENT_ID, "newone") self._fetch_token() def get_compute_resources(self) -> List[ComputeResource]: @@ -490,12 +515,17 @@ def delete_compute_resource(self, resource) -> int: def get_job_by_id(self, job_id: str) -> Optional[Job]: job = None url = f"{self.host}/jobs/{job_id}/" - response = requests.get(url, headers={ - 'Authorization': f'Bearer {self._token}' - }) + response = requests.get( + url, + headers={"Authorization": f"Bearer {self._token}"}, + timeout=REQUESTS_TIMEOUT, + ) if response.ok: data = json.loads(response.text) - job = Job(job_id=data.get("id"), job_client=GatewayJobClient(self.host, self._token)) + job = Job( + job_id=data.get("id"), + job_client=GatewayJobClient(self.host, self._token), + ) else: logging.warning(response.text) @@ -510,36 +540,38 @@ def run_program(self, program: Program) -> Job: with open(file_name, "rb") as file: response = requests.post( url=url, - data={ - "title": program.name, - "entrypoint": program.entrypoint - }, - files={ - "artifact": file - }, - headers={ - 'Authorization': f'Bearer {self._token}' - } + data={"title": program.name, "entrypoint": program.entrypoint}, + files={"artifact": file}, + headers={"Authorization": f"Bearer {self._token}"}, + timeout=REQUESTS_TIMEOUT, ) if not response.ok: - raise QuantumServerlessException(f"Something went wrong with program execution. {response.text}") + raise QuantumServerlessException( + f"Something went wrong with program execution. {response.text}" + ) json_response = json.loads(response.text) job_id = json_response.get("id") - # TODO: remove file + if os.path.exists(file_name): + os.remove(file_name) - return Job(job_id, job_client=GatewayJobClient(self.host, self._token)) + return Job(job_id, job_client=GatewayJobClient(self.host, self._token)) def get_jobs(self, **kwargs) -> List[Job]: jobs = [] url = f"{self.host}/jobs/" - response = requests.get(url, headers={ - 'Authorization': f'Bearer {self._token}' - }) + response = requests.get( + url, + headers={"Authorization": f"Bearer {self._token}"}, + timeout=REQUESTS_TIMEOUT, + ) if response.ok: jobs = [ - Job(job_id=job.get("id"), job_client=GatewayJobClient(self.host, self._token)) + Job( + job_id=job.get("id"), + job_client=GatewayJobClient(self.host, self._token), + ) for job in json.loads(response.text).get("results", []) ] else: @@ -548,16 +580,15 @@ def get_jobs(self, **kwargs) -> List[Job]: return jobs def _fetch_token(self): - realm = "Test" # TODO: get realm - client_id = "newone" # TODO: get client id keycloak_response = requests.post( - url=f"{self.auth_host}/auth/realms/{realm}/protocol/openid-connect/token", + url=f"{self.auth_host}/auth/realms/{self._realm}/protocol/openid-connect/token", data={ "username": self._username, "password": self._password, - "client_id": client_id, - "grant_type": "password" - } + "client_id": self._client_id, + "grant_type": "password", + }, + timeout=REQUESTS_TIMEOUT, ) if not keycloak_response.ok: raise QuantumServerlessException("Incorrect credentials.") @@ -566,9 +597,8 @@ def _fetch_token(self): gateway_response = requests.post( url=f"{self.host}/dj-rest-auth/keycloak/", - data={ - "access_token": keycloak_token - } + data={"access_token": keycloak_token}, + timeout=REQUESTS_TIMEOUT, ) if not gateway_response.ok: diff --git a/client/quantum_serverless/quantum_serverless.py b/client/quantum_serverless/quantum_serverless.py index cd3c7d86d..60979d17c 100644 --- a/client/quantum_serverless/quantum_serverless.py +++ b/client/quantum_serverless/quantum_serverless.py @@ -30,12 +30,11 @@ import os import warnings from typing import Optional, Union, List, Dict, Any -from uuid import uuid4 import requests from ray._private.worker import BaseContext -from quantum_serverless.core.job import Job, RuntimeEnv +from quantum_serverless.core.job import Job from quantum_serverless.core.program import Program from quantum_serverless.core.provider import Provider, ComputeResource from quantum_serverless.exception import QuantumServerlessException @@ -46,7 +45,7 @@ class QuantumServerless: """QuantumServerless class.""" - def __init__(self, providers: Union[Provider, List[Provider]]): + def __init__(self, providers: Optional[Union[Provider, List[Provider]]] = None): """Quantum serverless management class. Args: @@ -55,7 +54,11 @@ def __init__(self, providers: Union[Provider, List[Provider]]): Raises: QuantumServerlessException """ - if isinstance(providers, Provider): + if providers is None: + providers = [ + Provider("local", compute_resource=ComputeResource(name="local")) + ] + elif isinstance(providers, Provider): providers = [providers] self._providers: List[Provider] = providers self._selected_provider: Provider = self._providers[-1] diff --git a/client/tests/core/test_job.py b/client/tests/core/test_job.py deleted file mode 100644 index 977ccf2ef..000000000 --- a/client/tests/core/test_job.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Tests jobs.""" -import os - -from ray.dashboard.modules.job.common import JobStatus -from testcontainers.compose import DockerCompose - -from quantum_serverless import QuantumServerless -from tests.utils import wait_for_job_client, wait_for_job_completion - -resources_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "../resources" -) - - -# pylint: disable=duplicate-code -def test_jobs(): - """Integration test for jobs.""" - - with DockerCompose( - resources_path, compose_file_name="test-compose.yml", pull=True - ) as compose: - host = compose.get_service_host("testrayhead", 8265) - port = compose.get_service_port("testrayhead", 8265) - - serverless = QuantumServerless( - { - "providers": [ - { - "name": "test_docker", - "compute_resource": { - "name": "test_docker", - "host": host, - "port_job_server": port, - }, - } - ] - } - ).set_provider("test_docker") - - wait_for_job_client(serverless) - - job = serverless.run_job( - entrypoint="python job.py", - runtime_env={ - "working_dir": resources_path, - }, - ) - - wait_for_job_completion(job) - - assert "42" in job.logs() - assert job.status().is_terminal() - assert job.status() == JobStatus.SUCCEEDED - - recovered_job = serverless.get_job_by_id(job.job_id) - assert recovered_job.job_id == job.job_id - assert "42" in recovered_job.logs() - assert recovered_job.status().is_terminal() - assert recovered_job.status() == JobStatus.SUCCEEDED diff --git a/client/tests/core/test_program.py b/client/tests/core/test_program.py index ca54a8cb4..cb5b30a66 100644 --- a/client/tests/core/test_program.py +++ b/client/tests/core/test_program.py @@ -6,7 +6,8 @@ from ray.dashboard.modules.job.common import JobStatus from testcontainers.compose import DockerCompose -from quantum_serverless import QuantumServerless +from quantum_serverless import QuantumServerless, Provider +from quantum_serverless.core import ComputeResource from quantum_serverless.core.job import Job from quantum_serverless.core.program import Program from quantum_serverless.exception import QuantumServerlessException @@ -46,20 +47,13 @@ def test_program(): host = compose.get_service_host("testrayhead", 8265) port = compose.get_service_port("testrayhead", 8265) - serverless = QuantumServerless( - { - "providers": [ - { - "name": "docker", - "compute_resource": { - "name": "docker", - "host": host, - "port_job_server": port, - }, - } - ] - } - ).set_provider("docker") + provider = Provider( + name="docker", + compute_resource=ComputeResource( + name="docker", host=host, port_job_server=port + ), + ) + serverless = QuantumServerless(provider).set_provider("docker") wait_for_job_client(serverless) diff --git a/client/tests/core/test_state.py b/client/tests/core/test_state.py index a9ed2d31b..592ec90af 100644 --- a/client/tests/core/test_state.py +++ b/client/tests/core/test_state.py @@ -17,7 +17,8 @@ from ray.dashboard.modules.job.common import JobStatus from testcontainers.compose import DockerCompose -from quantum_serverless import QuantumServerless +from quantum_serverless import QuantumServerless, Program, Provider +from quantum_serverless.core import ComputeResource from quantum_serverless.core.state import RedisStateHandler from tests.utils import wait_for_job_client, wait_for_job_completion @@ -45,28 +46,18 @@ def test_state(): assert state_handler.get("some_key") == {"key": "value"} - serverless = QuantumServerless( - { - "providers": [ - { - "name": "test_docker", - "compute_resource": { - "name": "test_docker", - "host": host, - "port_job_server": port, - }, - } - ] - } - ).set_provider("test_docker") + provider = Provider( + name="test_docker", + compute_resource=ComputeResource( + name="test_docker", host=host, port_job_server=port + ), + ) + serverless = QuantumServerless(provider).set_provider("test_docker") wait_for_job_client(serverless) - job = serverless.run_job( - entrypoint="python job_with_state.py", - runtime_env={ - "working_dir": resources_path, - }, + job = serverless.run_program( + Program("test", entrypoint="job_with_state.py", working_dir=resources_path) ) wait_for_job_completion(job) diff --git a/client/tests/test_quantum_serverless.py b/client/tests/test_quantum_serverless.py index 29625640e..90d051a8d 100644 --- a/client/tests/test_quantum_serverless.py +++ b/client/tests/test_quantum_serverless.py @@ -52,7 +52,7 @@ def test_all_context_allocations(self): """Test context allocation from provider and compute_resource calls.""" serverless = QuantumServerless() - with serverless: + with serverless.context(): self.assertTrue(ray.is_initialized()) self.assertFalse(ray.is_initialized()) @@ -60,37 +60,6 @@ def test_all_context_allocations(self): self.assertTrue(ray.is_initialized()) self.assertFalse(ray.is_initialized()) - def test_load_config(self): - """Tests configuration loading.""" - config = { - "providers": [{"name": "local2", "compute_resource": {"name": "local2"}}] - } - - serverless = QuantumServerless(config) - self.assertEqual(len(serverless.providers()), 2) - - config2 = { - "providers": [ - { - "name": "some_provider", - "compute_resource": { - "name": "some_resource", - "host": "some_host", - "port_interactive": 10002, - }, - } - ] - } - serverless2 = QuantumServerless(config2) - self.assertEqual(len(serverless2.providers()), 2) - - compute_resource = serverless2.providers()[-1].compute_resource - - self.assertEqual(compute_resource.host, "some_host") - self.assertEqual(compute_resource.name, "some_resource") - self.assertEqual(compute_resource.port_interactive, 10002) - self.assertEqual(compute_resource.port_job_server, 8265) - def test_available_clusters_with_mock(self): """Test for external api call for available clusters.""" manager_address = "http://mock_host:42" diff --git a/gateway/requirements-dev.txt b/gateway/requirements-dev.txt new file mode 100644 index 000000000..a2f26b910 --- /dev/null +++ b/gateway/requirements-dev.txt @@ -0,0 +1,6 @@ +pylint>=2.9.5 +pytest>=6.2.5 +pylint-django>=2.5.3 +# Black's formatting rules can change between major versions, so we use +# the ~= specifier for it. +black~=22.1 diff --git a/gateway/requirements.txt b/gateway/requirements.txt index 73f482295..87b074a9f 100644 --- a/gateway/requirements.txt +++ b/gateway/requirements.txt @@ -5,3 +5,4 @@ django-allauth==0.52.0 dj-rest-auth==3.0.0 djangorestframework-simplejwt==5.2.2 ray[default]==2.3.0 +Django>=4.1.6 diff --git a/gateway/tox.ini b/gateway/tox.ini new file mode 100644 index 000000000..7ce967055 --- /dev/null +++ b/gateway/tox.ini @@ -0,0 +1,35 @@ +[tox] +minversion = 2.1 +envlist = py38, py39, py310, lint, coverage +# CI: skip-next-line +skipsdist = true +# CI: skip-next-line +skip_missing_interpreters = true + +[testenv] +# CI: skip-next-line +usedevelop = true +install_command = + pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} + LANGUAGE=en_US + LC_ALL=en_US.utf-8 + PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python +deps = -rrequirements.txt + -rrequirements-dev.txt +commands = + pip check + python manage.py test + +[testenv:lint] +envdir = .tox/lint +skip_install = true +commands = + black --check . + pylint --load-plugins pylint_django --load-plugins pylint_django.checkers.migrations --django-settings-module=gateway.settings --ignore api.migrations -rn api gateway + +[testenv:black] +envdir = .tox/lint +skip_install = true +commands = black . From 23448f1586d9045091f69fa98f84ef9b82526989 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Wed, 15 Mar 2023 15:36:12 -0400 Subject: [PATCH 05/15] Issue 254 | Gateway: linter + tests --- gateway/.pylintrc | 552 ++++++++++++++++++ gateway/api/admin.py | 8 +- gateway/api/apps.py | 8 +- gateway/api/management/__init__.py | 0 gateway/api/management/commands/__init__.py | 0 .../commands/create_compute_resource.py | 25 + .../commands/create_social_application.py | 41 ++ gateway/api/migrations/0001_initial.py | 20 +- gateway/api/migrations/0002_job.py | 26 +- .../api/migrations/0003_program_artifact.py | 9 +- .../0004_job_author_program_author.py | 23 +- .../api/migrations/0005_alter_job_result.py | 7 +- gateway/api/migrations/0006_job_status.py | 18 +- .../api/migrations/0007_computeresource.py | 19 +- .../migrations/0008_computeresource_users.py | 7 +- gateway/api/migrations/0009_job_ray_job_id.py | 7 +- .../migrations/0010_job_compute_resource.py | 14 +- .../api/migrations/0011_alter_job_status.py | 19 +- .../migrations/0012_alter_program_artifact.py | 25 + gateway/api/models.py | 30 +- gateway/api/permissions.py | 2 + gateway/api/serializers.py | 6 + gateway/api/tests.py | 3 - gateway/api/views.py | 47 +- gateway/gateway/settings.py | 70 ++- gateway/gateway/urls.py | 14 +- gateway/tests/__init__.py | 0 gateway/tests/api/__init__.py | 0 gateway/tests/api/test_job.py | 62 ++ gateway/tests/api/test_program.py | 50 ++ gateway/tests/fixtures/fixtures.json | 32 + 31 files changed, 1017 insertions(+), 127 deletions(-) create mode 100644 gateway/.pylintrc create mode 100644 gateway/api/management/__init__.py create mode 100644 gateway/api/management/commands/__init__.py create mode 100644 gateway/api/management/commands/create_compute_resource.py create mode 100644 gateway/api/management/commands/create_social_application.py create mode 100644 gateway/api/migrations/0012_alter_program_artifact.py delete mode 100644 gateway/api/tests.py create mode 100644 gateway/tests/__init__.py create mode 100644 gateway/tests/api/__init__.py create mode 100644 gateway/tests/api/test_job.py create mode 100644 gateway/tests/api/test_program.py create mode 100644 gateway/tests/fixtures/fixtures.json diff --git a/gateway/.pylintrc b/gateway/.pylintrc new file mode 100644 index 000000000..44a0fc58e --- /dev/null +++ b/gateway/.pylintrc @@ -0,0 +1,552 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Ignore function signatures when computing similarities. +ignore-signatures=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=10 + +# Maximum number of attributes for a class (see R0902). +max-attributes=8 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=builtins.BaseException, + builtins.Exception diff --git a/gateway/api/admin.py b/gateway/api/admin.py index e264716b7..8ef79e386 100644 --- a/gateway/api/admin.py +++ b/gateway/api/admin.py @@ -1,17 +1,19 @@ +"""Admin module.""" + from django.contrib import admin from .models import Job, Program, ComputeResource @admin.register(Program) class ProgramAdmin(admin.ModelAdmin): - pass + """ProgramAdmin.""" @admin.register(Job) class JobAdmin(admin.ModelAdmin): - pass + """JobAdmin.""" @admin.register(ComputeResource) class ComputeResourceAdmin(admin.ModelAdmin): - pass + """ComputeResourceAdmin.""" diff --git a/gateway/api/apps.py b/gateway/api/apps.py index 66656fd29..1aaa98ad5 100644 --- a/gateway/api/apps.py +++ b/gateway/api/apps.py @@ -1,6 +1,10 @@ +"""Applications.""" + from django.apps import AppConfig class ApiConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'api' + """ApiConfig.""" + + default_auto_field = "django.db.models.BigAutoField" + name = "api" diff --git a/gateway/api/management/__init__.py b/gateway/api/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gateway/api/management/commands/__init__.py b/gateway/api/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gateway/api/management/commands/create_compute_resource.py b/gateway/api/management/commands/create_compute_resource.py new file mode 100644 index 000000000..b0e2c6139 --- /dev/null +++ b/gateway/api/management/commands/create_compute_resource.py @@ -0,0 +1,25 @@ +"""Create compute resource command.""" + +from django.core.management.base import BaseCommand + +from api.models import ComputeResource + + +class Command(BaseCommand): + """Create compute resource command.""" + + help = "Creates default compute resource." + + def add_arguments(self, parser): + parser.add_argument("host", type=str, help="Host of compute resource.") + + def handle(self, *args, **options): + host = options.get("host") + compute_resource = ComputeResource(title="Ray cluster default", host=host) + compute_resource.save() + + self.stdout.write( + self.style.SUCCESS( + f"Successfully created compute resource {compute_resource.title}" + ) + ) diff --git a/gateway/api/management/commands/create_social_application.py b/gateway/api/management/commands/create_social_application.py new file mode 100644 index 000000000..97d4e2b8e --- /dev/null +++ b/gateway/api/management/commands/create_social_application.py @@ -0,0 +1,41 @@ +"""Create social app command.""" + +from allauth.socialaccount.models import SocialApp +from django.contrib.sites.models import Site +from django.core.management.base import BaseCommand, CommandError + + +class Command(BaseCommand): + """Create social app command.""" + + help = "Creates keycloak social app and default site." + + def add_arguments(self, parser): + parser.add_argument( + "--host", + type=str, + help="Django site host. Also used for keycloak social app.", + ) + parser.add_argument("--client_id", type=str, help="Keycloak client id.") + + def handle(self, *args, **options): + host = options.get("host") + client_id = options.get("client_id") + + if host is None or client_id is None: + raise CommandError("Arguments [host] and [client_id] must be provided.") + + site = Site.objects.filter(domain=host).first() + if site is None: + site = Site(domain=host, name=host) + site.save() + + social_app = SocialApp.objects.filter(provider="keycloak").first() + if social_app is None: + social_app = SocialApp( + provider="keycloak", name="keycloak", client_id=client_id + ) + social_app.sites.add(site) + social_app.save() + + self.stdout.write(self.style.SUCCESS("Done.")) diff --git a/gateway/api/migrations/0001_initial.py b/gateway/api/migrations/0001_initial.py index 25508a3cc..6952e6083 100644 --- a/gateway/api/migrations/0001_initial.py +++ b/gateway/api/migrations/0001_initial.py @@ -4,19 +4,25 @@ class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Program', + name="Program", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255)), - ('entrypoint', models.CharField(max_length=255)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("entrypoint", models.CharField(max_length=255)), ], ), ] diff --git a/gateway/api/migrations/0002_job.py b/gateway/api/migrations/0002_job.py index ac15fe51e..0aacbd6e4 100644 --- a/gateway/api/migrations/0002_job.py +++ b/gateway/api/migrations/0002_job.py @@ -5,18 +5,32 @@ class Migration(migrations.Migration): - dependencies = [ - ('api', '0001_initial'), + ("api", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Job', + name="Job", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('result', models.TextField()), - ('program', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.program')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("result", models.TextField()), + ( + "program", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="api.program", + ), + ), ], ), ] diff --git a/gateway/api/migrations/0003_program_artifact.py b/gateway/api/migrations/0003_program_artifact.py index 7785a3cb1..8619f44c4 100644 --- a/gateway/api/migrations/0003_program_artifact.py +++ b/gateway/api/migrations/0003_program_artifact.py @@ -4,16 +4,15 @@ class Migration(migrations.Migration): - dependencies = [ - ('api', '0002_job'), + ("api", "0002_job"), ] operations = [ migrations.AddField( - model_name='program', - name='artifact', - field=models.FileField(default='default', upload_to='artifacts_%Y_%m_%d'), + model_name="program", + name="artifact", + field=models.FileField(default="default", upload_to="artifacts_%Y_%m_%d"), preserve_default=False, ), ] diff --git a/gateway/api/migrations/0004_job_author_program_author.py b/gateway/api/migrations/0004_job_author_program_author.py index 41c0e21b6..882dd59b8 100644 --- a/gateway/api/migrations/0004_job_author_program_author.py +++ b/gateway/api/migrations/0004_job_author_program_author.py @@ -6,23 +6,30 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('api', '0003_program_artifact'), + ("api", "0003_program_artifact"), ] operations = [ migrations.AddField( - model_name='job', - name='author', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="job", + name="author", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), preserve_default=False, ), migrations.AddField( - model_name='program', - name='author', - field=models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="program", + name="author", + field=models.ForeignKey( + default="1", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), preserve_default=False, ), ] diff --git a/gateway/api/migrations/0005_alter_job_result.py b/gateway/api/migrations/0005_alter_job_result.py index 548978116..f6d89af16 100644 --- a/gateway/api/migrations/0005_alter_job_result.py +++ b/gateway/api/migrations/0005_alter_job_result.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('api', '0004_job_author_program_author'), + ("api", "0004_job_author_program_author"), ] operations = [ migrations.AlterField( - model_name='job', - name='result', + model_name="job", + name="result", field=models.TextField(blank=True, null=True), ), ] diff --git a/gateway/api/migrations/0006_job_status.py b/gateway/api/migrations/0006_job_status.py index 3e8bc522a..34458bb0a 100644 --- a/gateway/api/migrations/0006_job_status.py +++ b/gateway/api/migrations/0006_job_status.py @@ -4,15 +4,23 @@ class Migration(migrations.Migration): - dependencies = [ - ('api', '0005_alter_job_result'), + ("api", "0005_alter_job_result"), ] operations = [ migrations.AddField( - model_name='job', - name='status', - field=models.CharField(choices=[('PENDING', 'Pending'), ('RUNNING', 'Running'), ('ERROR', 'Error'), ('FINISHED', 'Finished')], default='PENDING', max_length=10), + model_name="job", + name="status", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("RUNNING", "Running"), + ("ERROR", "Error"), + ("FINISHED", "Finished"), + ], + default="PENDING", + max_length=10, + ), ), ] diff --git a/gateway/api/migrations/0007_computeresource.py b/gateway/api/migrations/0007_computeresource.py index 70766703e..98dc0eafe 100644 --- a/gateway/api/migrations/0007_computeresource.py +++ b/gateway/api/migrations/0007_computeresource.py @@ -4,18 +4,25 @@ class Migration(migrations.Migration): - dependencies = [ - ('api', '0006_job_status'), + ("api", "0006_job_status"), ] operations = [ migrations.CreateModel( - name='ComputeResource', + name="ComputeResource", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), - ('host', models.CharField(max_length=100)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), + ("host", models.CharField(max_length=100)), ], ), ] diff --git a/gateway/api/migrations/0008_computeresource_users.py b/gateway/api/migrations/0008_computeresource_users.py index 26b463caa..6cb85ace6 100644 --- a/gateway/api/migrations/0008_computeresource_users.py +++ b/gateway/api/migrations/0008_computeresource_users.py @@ -5,16 +5,15 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('api', '0007_computeresource'), + ("api", "0007_computeresource"), ] operations = [ migrations.AddField( - model_name='computeresource', - name='users', + model_name="computeresource", + name="users", field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), ), ] diff --git a/gateway/api/migrations/0009_job_ray_job_id.py b/gateway/api/migrations/0009_job_ray_job_id.py index b4edbd055..f2b12ce13 100644 --- a/gateway/api/migrations/0009_job_ray_job_id.py +++ b/gateway/api/migrations/0009_job_ray_job_id.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('api', '0008_computeresource_users'), + ("api", "0008_computeresource_users"), ] operations = [ migrations.AddField( - model_name='job', - name='ray_job_id', + model_name="job", + name="ray_job_id", field=models.CharField(blank=True, max_length=255, null=True), ), ] diff --git a/gateway/api/migrations/0010_job_compute_resource.py b/gateway/api/migrations/0010_job_compute_resource.py index e6883a8cd..65d8f3926 100644 --- a/gateway/api/migrations/0010_job_compute_resource.py +++ b/gateway/api/migrations/0010_job_compute_resource.py @@ -5,15 +5,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('api', '0009_job_ray_job_id'), + ("api", "0009_job_ray_job_id"), ] operations = [ migrations.AddField( - model_name='job', - name='compute_resource', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.computeresource'), + model_name="job", + name="compute_resource", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="api.computeresource", + ), ), ] diff --git a/gateway/api/migrations/0011_alter_job_status.py b/gateway/api/migrations/0011_alter_job_status.py index dd24b5563..33dc31e70 100644 --- a/gateway/api/migrations/0011_alter_job_status.py +++ b/gateway/api/migrations/0011_alter_job_status.py @@ -4,15 +4,24 @@ class Migration(migrations.Migration): - dependencies = [ - ('api', '0010_job_compute_resource'), + ("api", "0010_job_compute_resource"), ] operations = [ migrations.AlterField( - model_name='job', - name='status', - field=models.CharField(choices=[('PENDING', 'Pending'), ('RUNNING', 'Running'), ('STOPPED', 'Stopped'), ('SUCCEEDED', 'Succeeded'), ('FAILED', 'Failed')], default='PENDING', max_length=10), + model_name="job", + name="status", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("RUNNING", "Running"), + ("STOPPED", "Stopped"), + ("SUCCEEDED", "Succeeded"), + ("FAILED", "Failed"), + ], + default="PENDING", + max_length=10, + ), ), ] diff --git a/gateway/api/migrations/0012_alter_program_artifact.py b/gateway/api/migrations/0012_alter_program_artifact.py new file mode 100644 index 000000000..8ac0feff7 --- /dev/null +++ b/gateway/api/migrations/0012_alter_program_artifact.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1 on 2023-03-15 17:49 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0011_alter_job_status"), + ] + + operations = [ + migrations.AlterField( + model_name="program", + name="artifact", + field=models.FileField( + upload_to="artifacts_%Y_%m_%d", + validators=[ + django.core.validators.FileExtensionValidator( + allowed_extensions=["tar"] + ) + ], + ), + ), + ] diff --git a/gateway/api/models.py b/gateway/api/models.py index 8d542d208..f31395d2d 100644 --- a/gateway/api/models.py +++ b/gateway/api/models.py @@ -1,11 +1,20 @@ +"""Models.""" +from django.core.validators import FileExtensionValidator from django.db import models from django.conf import settings class Program(models.Model): + """Program model.""" + title = models.CharField(max_length=255) entrypoint = models.CharField(max_length=255) - artifact = models.FileField(upload_to="artifacts_%Y_%m_%d", null=False, blank=False) + artifact = models.FileField( + upload_to="artifacts_%Y_%m_%d", + null=False, + blank=False, + validators=[FileExtensionValidator(allowed_extensions=["tar"])], + ) author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, @@ -15,8 +24,9 @@ def __str__(self): return f"{self.title}" -# TODO: create command to create default cluster class ComputeResource(models.Model): + """Compute resource model.""" + title = models.CharField(max_length=100, blank=False, null=False) host = models.CharField(max_length=100, blank=False, null=False) @@ -27,17 +37,19 @@ def __str__(self): class Job(models.Model): + """Job model.""" + PENDING = "PENDING" RUNNING = "RUNNING" STOPPED = "STOPPED" SUCCEEDED = "SUCCEEDED" FAILED = "FAILED" JOB_STATUSES = [ - (PENDING, 'Pending'), - (RUNNING, 'Running'), - (STOPPED, 'Stopped'), - (SUCCEEDED, 'Succeeded'), - (FAILED, 'Failed') + (PENDING, "Pending"), + (RUNNING, "Running"), + (STOPPED, "Stopped"), + (SUCCEEDED, "Succeeded"), + (FAILED, "Failed"), ] program = models.ForeignKey(to=Program, on_delete=models.SET_NULL, null=True) @@ -51,7 +63,9 @@ class Job(models.Model): choices=JOB_STATUSES, default=PENDING, ) - compute_resource = models.ForeignKey(ComputeResource, on_delete=models.SET_NULL, null=True, blank=True) + compute_resource = models.ForeignKey( + ComputeResource, on_delete=models.SET_NULL, null=True, blank=True + ) ray_job_id = models.CharField(max_length=255, null=True, blank=True) def __str__(self): diff --git a/gateway/api/permissions.py b/gateway/api/permissions.py index 6552f10de..f88acf99e 100644 --- a/gateway/api/permissions.py +++ b/gateway/api/permissions.py @@ -1,3 +1,5 @@ +"""Permissions.""" + from rest_framework import permissions diff --git a/gateway/api/serializers.py b/gateway/api/serializers.py index 2caa750a1..a7573a00a 100644 --- a/gateway/api/serializers.py +++ b/gateway/api/serializers.py @@ -1,15 +1,21 @@ +"""Serializers.""" + from rest_framework import serializers from api.models import Program, Job class ProgramSerializer(serializers.ModelSerializer): + """ProgramSerializer.""" + class Meta: model = Program fields = ["title", "entrypoint", "artifact"] class JobSerializer(serializers.ModelSerializer): + """JobSerializer.""" + class Meta: model = Job fields = ["id", "result", "status"] diff --git a/gateway/api/tests.py b/gateway/api/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/gateway/api/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/gateway/api/views.py b/gateway/api/views.py index e0d1bbc81..31f40041b 100644 --- a/gateway/api/views.py +++ b/gateway/api/views.py @@ -1,3 +1,5 @@ +"""Views.""" + import json import os.path import shutil @@ -7,9 +9,9 @@ from django.conf import settings from ray.dashboard.modules.job.common import JobStatus from ray.dashboard.modules.job.sdk import JobSubmissionClient -from rest_framework import viewsets, permissions, status from allauth.socialaccount.providers.keycloak.views import KeycloakOAuth2Adapter from dj_rest_auth.registration.views import SocialLoginView +from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 from rest_framework.response import Response @@ -19,7 +21,10 @@ from .serializers import ProgramSerializer, JobSerializer +# pylint: disable=too-many-ancestors class ProgramViewSet(viewsets.ModelViewSet): + """ProgramViewSet.""" + queryset = Program.objects.all() serializer_class = ProgramSerializer permission_classes = [permissions.IsAuthenticated] @@ -32,17 +37,20 @@ def perform_create(self, serializer): @action(methods=["POST"], detail=False) def run_program(self, request): + """Runs provided program on compute resources.""" + serializer = self.get_serializer(data=request.data) if serializer.is_valid(): # create program program = Program(**serializer.data) - existing_programs = Program.objects.filter(author=request.user, title__exact=program.title) + existing_programs = Program.objects.filter( + author=request.user, title__exact=program.title + ) if existing_programs.count() > 0: # take existing one program = existing_programs.first() program.artifact = request.FILES.get("artifact") - # TODO: check format of file or create serializer program.author = request.user program.save() @@ -51,7 +59,7 @@ def run_program(self, request): if resources.count() == 0: return Response( {"error": "user do not have any resources in account"}, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) compute_resource = resources.first() @@ -59,26 +67,33 @@ def run_program(self, request): ray_client = JobSubmissionClient(compute_resource.host) # unpack data with tarfile.open(program.artifact.path) as file: - extract_folder = os.path.join(settings.MEDIA_ROOT, "tmp", str(uuid.uuid4())) + extract_folder = os.path.join( + settings.MEDIA_ROOT, "tmp", str(uuid.uuid4()) + ) file.extractall(extract_folder) ray_job_id = ray_client.submit_job( entrypoint=f"python {program.entrypoint}", - runtime_env={ - "working_dir": extract_folder - } + runtime_env={"working_dir": extract_folder}, ) # remote temp data if os.path.exists(extract_folder): shutil.rmtree(extract_folder) - job = Job(program=program, author=request.user, ray_job_id=ray_job_id, compute_resource=compute_resource) + job = Job( + program=program, + author=request.user, + ray_job_id=ray_job_id, + compute_resource=compute_resource, + ) job.save() return Response(JobSerializer(job).data) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class JobViewSet(viewsets.ModelViewSet): + """JobViewSet.""" + queryset = Job.objects.all() serializer_class = JobSerializer permission_classes = [permissions.IsAuthenticated, IsOwner] @@ -89,7 +104,7 @@ def get_queryset(self): def perform_create(self, serializer): serializer.save(author=self.request.user) - def retrieve(self, request, pk=None): + def retrieve(self, request, pk=None): # pylint: disable=arguments-differ queryset = Job.objects.all() job: Job = get_object_or_404(queryset, pk=pk) serializer = JobSerializer(job) @@ -101,7 +116,8 @@ def retrieve(self, request, pk=None): return Response(serializer.data) @action(methods=["POST"], detail=True, permission_classes=[permissions.AllowAny]) - def result(self, request, pk=None): + def result(self, request, pk=None): # pylint: disable=invalid-name,unused-argument + """Save result of a job.""" job = self.get_object() job.result = json.dumps(request.data.get("result")) # job.status = Job.SUCCEEDED @@ -110,6 +126,8 @@ def result(self, request, pk=None): def ray_job_status_to_model_job_status(ray_job_status): + """Maps ray job status to model job status.""" + mapping = { JobStatus.PENDING: Job.PENDING, JobStatus.RUNNING: Job.RUNNING, @@ -119,5 +137,8 @@ def ray_job_status_to_model_job_status(ray_job_status): } return mapping.get(ray_job_status, Job.FAILED) + class KeycloakLogin(SocialLoginView): + """KeycloakLogin.""" + adapter_class = KeycloakOAuth2Adapter diff --git a/gateway/gateway/settings.py b/gateway/gateway/settings.py index a5e3bf421..57e04bf5e 100644 --- a/gateway/gateway/settings.py +++ b/gateway/gateway/settings.py @@ -38,17 +38,17 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - 'django.contrib.sites', + "django.contrib.sites", "rest_framework", - 'rest_framework.authtoken', - 'rest_framework_simplejwt', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'allauth.socialaccount.providers.keycloak', - 'dj_rest_auth', - 'dj_rest_auth.registration', - "api" + "rest_framework.authtoken", + "rest_framework_simplejwt", + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.keycloak", + "dj_rest_auth", + "dj_rest_auth.registration", + "api", ] MIDDLEWARE = [ @@ -66,8 +66,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / 'templates'] - , + "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -101,17 +100,22 @@ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, - {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, - {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, - {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, ] AUTHENTICATION_BACKENDS = [ # Needed to login by username in Django admin, regardless of `allauth` - 'django.contrib.auth.backends.ModelBackend', - + "django.contrib.auth.backends.ModelBackend", # `allauth` specific authentication methods, such as login by e-mail - 'allauth.account.auth_backends.AuthenticationBackend', + "allauth.account.auth_backends.AuthenticationBackend", ] # Internationalization @@ -139,19 +143,19 @@ REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" ], - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', - 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + "dj_rest_auth.jwt_auth.JWTCookieAuthentication", ), - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - 'PAGE_SIZE': 100 + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", + "PAGE_SIZE": 100, } REST_AUTH = { - 'USE_JWT': True, + "USE_JWT": True, # 'JWT_AUTH_COOKIE': 'gateway-app-auth', # 'JWT_AUTH_REFRESH_COOKIE': 'gateway-refresh-token', } @@ -162,16 +166,18 @@ SETTING_KEYCLOAK_URL = "SETTING_KEYCLOAK_URL" SETTING_KEYCLOAK_REALM = "SETTING_KEYCLOAK_REALM" SOCIALACCOUNT_PROVIDERS = { - 'keycloak': { # TODO: via env vars - 'KEYCLOAK_URL': os.environ.get(SETTING_KEYCLOAK_URL, 'http://localhost:8085/auth'), - 'KEYCLOAK_REALM': os.environ.get(SETTING_KEYCLOAK_REALM, 'Test') + "keycloak": { + "KEYCLOAK_URL": os.environ.get( + SETTING_KEYCLOAK_URL, "http://localhost:8085/auth" + ), + "KEYCLOAK_REALM": os.environ.get(SETTING_KEYCLOAK_REALM, "Test"), } } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=10), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=20) + "ACCESS_TOKEN_LIFETIME": timedelta(days=10), + "REFRESH_TOKEN_LIFETIME": timedelta(days=20), } MEDIA_ROOT = os.path.join(BASE_DIR, "media") -MEDIA_URL = '/media/' +MEDIA_URL = "/media/" diff --git a/gateway/gateway/urls.py b/gateway/gateway/urls.py index 0a5971bc2..7f76efbea 100644 --- a/gateway/gateway/urls.py +++ b/gateway/gateway/urls.py @@ -20,15 +20,15 @@ from api.views import ProgramViewSet, KeycloakLogin, JobViewSet router = routers.DefaultRouter() -router.register(r'programs', ProgramViewSet) -router.register(r'jobs', JobViewSet) +router.register(r"programs", ProgramViewSet) +router.register(r"jobs", JobViewSet) urlpatterns = [ - path('', include(router.urls)), - path('dj-rest-auth/', include('dj_rest_auth.urls')), - path('dj-rest-auth/keycloak/', KeycloakLogin.as_view(), name='keycloak_login'), - path('accounts/', include('allauth.urls')), - path('api-auth/', include('rest_framework.urls')), + path("", include(router.urls)), + path("dj-rest-auth/", include("dj_rest_auth.urls")), + path("dj-rest-auth/keycloak/", KeycloakLogin.as_view(), name="keycloak_login"), + path("accounts/", include("allauth.urls")), + path("api-auth/", include("rest_framework.urls")), path("admin/", admin.site.urls), ] diff --git a/gateway/tests/__init__.py b/gateway/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gateway/tests/api/__init__.py b/gateway/tests/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gateway/tests/api/test_job.py b/gateway/tests/api/test_job.py new file mode 100644 index 000000000..3f4843b75 --- /dev/null +++ b/gateway/tests/api/test_job.py @@ -0,0 +1,62 @@ +"""Tests jobs APIs.""" + +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + + +class TestJobApi(APITestCase): + """TestJobApi.""" + + fixtures = ["tests/fixtures/fixtures.json"] + + def _authorize(self): + """Authorize client.""" + auth = reverse("rest_login") + resp = self.client.post( + auth, {"username": "test_user", "password": "123"}, format="json" + ) + token = resp.data.get("access_token") + self.client.credentials(HTTP_AUTHORIZATION="Bearer " + token) + + def test_job_non_auth_user(self): + """Tests job list non-authorized.""" + url = reverse("job-list") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_job_list(self): + """Tests job list authorized.""" + self._authorize() + + jobs_response = self.client.get(reverse("job-list"), format="json") + self.assertEqual(jobs_response.status_code, status.HTTP_200_OK) + self.assertEqual(jobs_response.data.get("count"), 1) + self.assertEqual( + jobs_response.data.get("results")[0].get("status"), "SUCCEEDED" + ) + self.assertEqual( + jobs_response.data.get("results")[0].get("result"), '{"somekey":1}' + ) + + def test_job_detail(self): + """Tests job detail authorized.""" + self._authorize() + + jobs_response = self.client.get(reverse("job-detail", args=[1]), format="json") + self.assertEqual(jobs_response.status_code, status.HTTP_200_OK) + self.assertEqual(jobs_response.data.get("status"), "SUCCEEDED") + self.assertEqual(jobs_response.data.get("result"), '{"somekey":1}') + + def test_job_save_result(self): + """Tests job results save.""" + self._authorize() + + jobs_response = self.client.post( + reverse("job-result", args=[1]), + format="json", + data={"result": {"ultimate": 42}}, + ) + self.assertEqual(jobs_response.status_code, status.HTTP_200_OK) + self.assertEqual(jobs_response.data.get("status"), "SUCCEEDED") + self.assertEqual(jobs_response.data.get("result"), '{"ultimate": 42}') diff --git a/gateway/tests/api/test_program.py b/gateway/tests/api/test_program.py new file mode 100644 index 000000000..463ff0ca7 --- /dev/null +++ b/gateway/tests/api/test_program.py @@ -0,0 +1,50 @@ +"""Tests program APIs.""" + +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + + +class TestProgramApi(APITestCase): + """TestProgramApi.""" + + fixtures = ["tests/fixtures/fixtures.json"] + + def test_programs_non_auth_user(self): + """Tests program list non-authorized.""" + url = reverse("program-list") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_programs_list(self): + """Tests programs list authorized.""" + + auth = reverse("rest_login") + resp = self.client.post( + auth, {"username": "test_user", "password": "123"}, format="json" + ) + token = resp.data.get("access_token") + self.client.credentials(HTTP_AUTHORIZATION="Bearer " + token) + + programs_response = self.client.get(reverse("program-list"), format="json") + self.assertEqual(programs_response.status_code, status.HTTP_200_OK) + self.assertEqual(programs_response.data.get("count"), 1) + self.assertEqual( + programs_response.data.get("results")[0].get("title"), "program" + ) + + def test_program_detail(self): + """Tests program detail authorized.""" + auth = reverse("rest_login") + resp = self.client.post( + auth, {"username": "test_user", "password": "123"}, format="json" + ) + token = resp.data.get("access_token") + self.client.credentials(HTTP_AUTHORIZATION="Bearer " + token) + + programs_response = self.client.get( + reverse("program-detail", args=[1]), format="json" + ) + self.assertEqual(programs_response.status_code, status.HTTP_200_OK) + self.assertEqual(programs_response.data.get("title"), "program") + self.assertEqual(programs_response.data.get("entrypoint"), "program.py") diff --git a/gateway/tests/fixtures/fixtures.json b/gateway/tests/fixtures/fixtures.json new file mode 100644 index 000000000..afce51459 --- /dev/null +++ b/gateway/tests/fixtures/fixtures.json @@ -0,0 +1,32 @@ +[ + { + "model": "auth.user", + "pk": 1, + "fields": { + "email": "test_user@email.com", + "username": "test_user", + "password": "pbkdf2_sha256$390000$kcex1rxhZg6VVJYkx71cBX$e4ns0xDykbO6Dz6j4nZ4uNusqkB9GVpojyegPv5/9KM=", + "is_active": true + } + }, + { + "model": "api.program", + "pk": 1, + "fields": { + "title": "program", + "entrypoint": "program.py", + "artifact": "path", + "author": 1 + } + }, + { + "model": "api.job", + "pk": 1, + "fields": { + "program": 1, + "result": "{\"somekey\":1}", + "status": "SUCCEEDED", + "author": 1 + } + } +] \ No newline at end of file From 2055b749ec76a266e8c29e9050d740d04c0bf4ed Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Wed, 15 Mar 2023 16:46:13 -0400 Subject: [PATCH 06/15] Issue 254 | Gateway: dockerfile --- .github/workflows/gateway-verify.yaml | 44 +++++++++++++++++++ docker-compose-dev.yml | 16 ++++++- gateway/.dockerignore | 28 ++++++++++++ gateway/Dockerfile | 30 +++++++++++++ .../commands/create_social_application.py | 5 ++- gateway/entrypoint.sh | 9 ++++ gateway/gateway/settings.py | 4 +- gateway/requirements.txt | 1 + 8 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/gateway-verify.yaml create mode 100644 gateway/.dockerignore create mode 100644 gateway/Dockerfile create mode 100755 gateway/entrypoint.sh diff --git a/.github/workflows/gateway-verify.yaml b/.github/workflows/gateway-verify.yaml new file mode 100644 index 000000000..988c4de98 --- /dev/null +++ b/.github/workflows/gateway-verify.yaml @@ -0,0 +1,44 @@ +name: Gateway verify process + +on: + pull_request: + +jobs: + verify-gateway: + name: lint, test + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9'] + + defaults: + run: + working-directory: ./gateway + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Set up tox environment + run: | + pver=${{ matrix.python-version }} + tox_env="-epy${pver/./}" + echo tox_env + echo TOX_ENV=$tox_env >> $GITHUB_ENV + + - name: Install tox + run: | + pip install tox + + - name: Run styles check + run: tox -elint + + - name: Test using tox environment + run: | + tox ${{ env.TOX_ENV }} diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index e23611d41..779a61115 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -22,8 +22,6 @@ services: - OT_JAEGER_HOST_KEY=jaeger ports: - 8265:8265 - - 8000:8000 - - 10001:10001 privileged: true volumes: - /dev/shm:/dev/shm @@ -53,5 +51,19 @@ services: - 9411:9411 networks: - safe-tier + gateway: + container_name: gateway + build: ./gateway + command: gunicorn gateway.wsgi:application --bind 0.0.0.0:8000 + ports: + - 8000:8000 + environment: + - DEBUG=0 + - SITE_HOST=127.0.0.1 + - RAY_HOST=http://ray:8265 + - CLIENT_ID=newone + - DJANGO_SUPERUSER_USERNAME=admin + - DJANGO_SUPERUSER_PASSWORD=123 + - DJANGO_SUPERUSER_EMAIL=admin@noemail.com networks: safe-tier: diff --git a/gateway/.dockerignore b/gateway/.dockerignore new file mode 100644 index 000000000..f1eb641e1 --- /dev/null +++ b/gateway/.dockerignore @@ -0,0 +1,28 @@ +*.pyc +*.pyo +*.mo +*.db +*.css.map +*.egg-info +*.sql.gz +.cache +.project +.idea +.pydevproject +.idea/workspace.xml +.DS_Store +.git/ +.sass-cache +.vagrant/ +__pycache__ +dist +docs +env +logs +src/{{ project_name }}/settings/local.py +src/node_modules +web/media +web/static/CACHE +stats +Dockerfile +*.sqlite3 diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 000000000..127c90ced --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,30 @@ +# pull official base image +FROM python:3.9.16-slim-buster + +# set work directory +WORKDIR /usr/src/app + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install psycopg2 dependencies +#RUN apk update \ +# && apk add postgresql-dev gcc python3-dev musl-dev + + +# install dependencies +RUN pip install --upgrade pip +COPY ./requirements.txt . +RUN pip install -r requirements.txt + +# copy entrypoint.sh +COPY ./entrypoint.sh . +RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh +RUN chmod +x /usr/src/app/entrypoint.sh + +# copy project +COPY . . + +# run entrypoint.sh +ENTRYPOINT ["/usr/src/app/entrypoint.sh"] diff --git a/gateway/api/management/commands/create_social_application.py b/gateway/api/management/commands/create_social_application.py index 97d4e2b8e..650bd7081 100644 --- a/gateway/api/management/commands/create_social_application.py +++ b/gateway/api/management/commands/create_social_application.py @@ -35,7 +35,10 @@ def handle(self, *args, **options): social_app = SocialApp( provider="keycloak", name="keycloak", client_id=client_id ) + social_app.save() social_app.sites.add(site) social_app.save() - self.stdout.write(self.style.SUCCESS("Done.")) + self.stdout.write( + self.style.SUCCESS("Successfully created site and keycloak app.") + ) diff --git a/gateway/entrypoint.sh b/gateway/entrypoint.sh new file mode 100755 index 000000000..e2ef61daa --- /dev/null +++ b/gateway/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +python manage.py migrate +python manage.py createsuperuser --noinput + +python manage.py create_social_application --host="$SITE_HOST" --client_id="$CLIENT_ID" +python manage.py create_compute_resource "$RAY_HOST" + +exec "$@" diff --git a/gateway/gateway/settings.py b/gateway/gateway/settings.py index 57e04bf5e..1fffecf63 100644 --- a/gateway/gateway/settings.py +++ b/gateway/gateway/settings.py @@ -24,9 +24,9 @@ SECRET_KEY = "django-insecure-&)i3b5aue*#-i6k9i-03qm(d!0h&662lbhj12on_*gimn3x8p7" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.environ.get("DEBUG", 1) -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",") # Application definition diff --git a/gateway/requirements.txt b/gateway/requirements.txt index 87b074a9f..a62eed32d 100644 --- a/gateway/requirements.txt +++ b/gateway/requirements.txt @@ -6,3 +6,4 @@ dj-rest-auth==3.0.0 djangorestframework-simplejwt==5.2.2 ray[default]==2.3.0 Django>=4.1.6 +gunicorn==20.1.0 From b0d20eaa5a71c3a76ba4b13c910bf1f07a4629f2 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Fri, 17 Mar 2023 10:55:16 -0400 Subject: [PATCH 07/15] Issue 254 | Gateway: act as gateway and hide keycloak behind --- Makefile | 11 ++- docker-compose.yml | 35 +++++++-- gateway/README.md | 27 +++++++ .../commands/create_social_application.py | 7 +- gateway/api/views.py | 72 ++++++++++++++++++- gateway/gateway/settings.py | 8 ++- gateway/gateway/urls.py | 3 +- realm-export.json | 31 +++++--- 8 files changed, 174 insertions(+), 20 deletions(-) create mode 100644 gateway/README.md diff --git a/Makefile b/Makefile index 4974c2cea..54e3bd5ae 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ repository=qiskit notebookImageName=$(repository)/quantum-serverless-notebook rayNodeImageName=$(repository)/quantum-serverless-ray-node +gatewayImageName=$(repository)/quantum-serverless-gateway # ============= # Docker images @@ -14,8 +15,8 @@ rayNodeImageName=$(repository)/quantum-serverless-ray-node build-and-push: build-all push-all -build-all: build-notebook build-ray-node -push-all: push-notebook push-ray-node +build-all: build-notebook build-ray-node build-gateway +push-all: push-notebook push-ray-node push-gateway build-notebook: docker build -t $(notebookImageName):$(version) -f ./infrastructure/docker/Dockerfile-notebook . @@ -23,8 +24,14 @@ build-notebook: build-ray-node: docker build -t $(rayNodeImageName):$(version) -f ./infrastructure/docker/Dockerfile-ray-qiskit . +build-gateway: + docker build -t $(gatewayImageName):$(version) -f ./gateway/Dockerfile . + push-notebook: docker push $(notebookImageName):$(version) push-ray-node: docker push $(rayNodeImageName):$(version) + +push-gateway: + docker push $(gatewayImageName):$(version) diff --git a/docker-compose.yml b/docker-compose.yml index 6b75bcab7..92ba42f32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: # image: qiskit/quantum-serverless-notebook:nightly-py39 # ports: # - 8888:8888 +# environment: +# - JUPYTER_TOKEN=123 # networks: # - safe-tier ray-head: @@ -18,8 +20,6 @@ services: - OT_JAEGER_HOST_KEY=jaeger ports: - 8265:8265 -# - 8000:8000 -# - 10001:10001 privileged: true volumes: - /dev/shm:/dev/shm @@ -55,10 +55,12 @@ services: POSTGRES_DB: testkeycloakdb POSTGRES_USER: testkeycloakuser POSTGRES_PASSWORD: testkeycloakpassword + networks: + - safe-tier restart: always - keycloak: + container_name: keycloak image: jboss/keycloak:16.1.0 volumes: - ./realm-export.json:/opt/jboss/keycloak/imports/realm-export.json @@ -71,15 +73,38 @@ services: DB_USER: testkeycloakuser DB_SCHEMA: public DB_PASSWORD: testkeycloakpassword - KEYCLOAK_USER: keycloakuser - KEYCLOAK_PASSWORD: keycloakpassword + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: 123 PROXY_ADDRESS_FORWARDING: "true" KEYCLOAK_LOGLEVEL: DEBUG ports: - '8085:8080' depends_on: - postgres + networks: + - safe-tier restart: always + gateway: + container_name: gateway + image: docker.io/qiskit/quantum-serverless-gateway:nightly + command: gunicorn gateway.wsgi:application --bind 0.0.0.0:8000 --workers=3 + ports: + - 8000:8000 + environment: + - DEBUG=0 + - RAY_HOST=http://ray-head:8265 + - CLIENT_ID=gateway-client + - DJANGO_SUPERUSER_USERNAME=admin + - DJANGO_SUPERUSER_PASSWORD=123 + - DJANGO_SUPERUSER_EMAIL=admin@noemail.com + - SETTING_KEYCLOAK_URL=http://keycloak:8080/auth + - SETTING_KEYCLOAK_REALM=Test + - SETTINGS_KEYCLOAK_CLIENT_SECRET=AQ3sZ4eiF7NhOtfxeUEGo0YN7uQBoUnO + - SITE_HOST=http://gateway:8000 + networks: + - safe-tier + depends_on: + - keycloak networks: safe-tier: diff --git a/gateway/README.md b/gateway/README.md new file mode 100644 index 000000000..1ecf41502 --- /dev/null +++ b/gateway/README.md @@ -0,0 +1,27 @@ +Quantum serverless gateway +========================== + +Gateway is a set of apis that are used as a backend for providers. + +### Build image + +```shell +docker build -t qiskit/quantum-serverless-gateway: . +``` + +### Env variables for container + +| Variable | Description | +|---------------------------------|---------------------------------------------------------------------------------| +| DEBUG | run application on debug mode | +| SITE_HOST | host of site that will be created for Django application | +| RAY_HOST | Host of Ray head node that will be assigned to default created compute resource | +| CLIENT_ID | Keycloak client id that will be created for social integrations | +| DJANGO_SUPERUSER_USERNAME | username for admin user that is created on launch of container | +| DJANGO_SUPERUSER_PASSWORD | password for admin user that is created on launch of container | +| DJANGO_SUPERUSER_EMAIL | email for admin user that is created on launch of container | +| SETTING_KEYCLOAK_URL | url to keycloak instance | +| SETTING_KEYCLOAK_REALM | Realm of keycloak to authenticate with | +| SETTINGS_KEYCLOAK_CLIENT_SECRET | client secret | +| SETTINGS_KEYCLOAK_CLIENT_NAME | client name | +| SITE_HOST | host of application | diff --git a/gateway/api/management/commands/create_social_application.py b/gateway/api/management/commands/create_social_application.py index 650bd7081..84d423e3a 100644 --- a/gateway/api/management/commands/create_social_application.py +++ b/gateway/api/management/commands/create_social_application.py @@ -25,9 +25,14 @@ def handle(self, *args, **options): if host is None or client_id is None: raise CommandError("Arguments [host] and [client_id] must be provided.") - site = Site.objects.filter(domain=host).first() + site = Site.objects.filter().first() if site is None: + self.stdout.write( + self.style.SUCCESS("Site did not exists. Creating new one.") + ) site = Site(domain=host, name=host) + site.domain = host + site.name = host site.save() social_app = SocialApp.objects.filter(provider="keycloak").first() diff --git a/gateway/api/views.py b/gateway/api/views.py index 31f40041b..0bffe50e2 100644 --- a/gateway/api/views.py +++ b/gateway/api/views.py @@ -6,15 +6,18 @@ import tarfile import uuid +import requests +from allauth.socialaccount.providers.keycloak.views import KeycloakOAuth2Adapter +from dj_rest_auth.registration.views import SocialLoginView from django.conf import settings +from django.contrib.auth.models import User # pylint: disable=imported-auth-user from ray.dashboard.modules.job.common import JobStatus from ray.dashboard.modules.job.sdk import JobSubmissionClient -from allauth.socialaccount.providers.keycloak.views import KeycloakOAuth2Adapter -from dj_rest_auth.registration.views import SocialLoginView from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 from rest_framework.response import Response +from rest_framework.views import APIView from .models import Program, Job, ComputeResource from .permissions import IsOwner @@ -142,3 +145,68 @@ class KeycloakLogin(SocialLoginView): """KeycloakLogin.""" adapter_class = KeycloakOAuth2Adapter + + +class KeycloakUsersView(APIView): + """KeycloakUsersView.""" + + queryset = User.objects.all() + permission_classes = [permissions.AllowAny] + + def post(self, request): + """Get application token. + + Request: /POST + Body: {"username": ..., "password": ...} + """ + keycloak_payload = { + "grant_type": "password", + "client_id": settings.SETTINGS_KEYCLOAK_CLIENT_NAME, + "client_secret": settings.SETTINGS_KEYCLOAK_CLIENT_SECRET, + } + keycloak_provider = settings.SOCIALACCOUNT_PROVIDERS.get("keycloak") + if keycloak_provider is None: + return Response( + { + "message": "Oops. Provider was not configured correctly on a server side." + }, + status=status.HTTP_501_NOT_IMPLEMENTED, + ) + + keycloak_url = ( + f"{keycloak_provider.get('KEYCLOAK_URL')}/realms/" + f"{keycloak_provider.get('KEYCLOAK_REALM')}/" + f"protocol/openid-connect/token/" + ) + payload = {**keycloak_payload, **request.data} + keycloak_response = requests.post( + keycloak_url, + data=payload, + timeout=settings.SETTINGS_KEYCLOAK_REQUESTS_TIMEOUT, + ) + if not keycloak_response.ok: + return Response( + {"message": keycloak_response.text}, status=status.HTTP_400_BAD_REQUEST + ) + + access_token = json.loads(keycloak_response.text).get("access_token") + if settings.SITE_HOST is None: + return Response( + { + "message": "Oops. Application was not configured correctly on a server side." + }, + status=status.HTTP_501_NOT_IMPLEMENTED, + ) + + rest_auth_url = f"{settings.SITE_HOST}/dj-rest-auth/keycloak/" + rest_auth_response = requests.post( + rest_auth_url, + json={"access_token": access_token}, + timeout=settings.SETTINGS_KEYCLOAK_REQUESTS_TIMEOUT, + ) + + if not rest_auth_response.ok: + return Response( + {"message": rest_auth_response.text}, status=status.HTTP_400_BAD_REQUEST + ) + return Response(json.loads(rest_auth_response.text)) diff --git a/gateway/gateway/settings.py b/gateway/gateway/settings.py index 1fffecf63..9573c2c51 100644 --- a/gateway/gateway/settings.py +++ b/gateway/gateway/settings.py @@ -24,7 +24,7 @@ SECRET_KEY = "django-insecure-&)i3b5aue*#-i6k9i-03qm(d!0h&662lbhj12on_*gimn3x8p7" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.environ.get("DEBUG", 1) +DEBUG = int(os.environ.get("DEBUG", 1)) ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",") @@ -161,10 +161,16 @@ } SITE_ID = 1 +SITE_HOST = os.environ.get("SITE_HOST") # Provider specific settings SETTING_KEYCLOAK_URL = "SETTING_KEYCLOAK_URL" SETTING_KEYCLOAK_REALM = "SETTING_KEYCLOAK_REALM" +SETTINGS_KEYCLOAK_CLIENT_NAME = os.environ.get("CLIENT_ID") +SETTINGS_KEYCLOAK_CLIENT_SECRET = os.environ.get("SETTINGS_KEYCLOAK_CLIENT_SECRET") +SETTINGS_KEYCLOAK_REQUESTS_TIMEOUT = int( + os.environ.get("SETTINGS_KEYCLOAK_REQUESTS_TIMEOUT", 15) +) SOCIALACCOUNT_PROVIDERS = { "keycloak": { "KEYCLOAK_URL": os.environ.get( diff --git a/gateway/gateway/urls.py b/gateway/gateway/urls.py index 7f76efbea..5c3e94c08 100644 --- a/gateway/gateway/urls.py +++ b/gateway/gateway/urls.py @@ -17,7 +17,7 @@ from django.urls import path, include from rest_framework import routers -from api.views import ProgramViewSet, KeycloakLogin, JobViewSet +from api.views import ProgramViewSet, KeycloakLogin, JobViewSet, KeycloakUsersView router = routers.DefaultRouter() router.register(r"programs", ProgramViewSet) @@ -28,6 +28,7 @@ path("", include(router.urls)), path("dj-rest-auth/", include("dj_rest_auth.urls")), path("dj-rest-auth/keycloak/", KeycloakLogin.as_view(), name="keycloak_login"), + path("dj-rest-auth/keycloak/login/", KeycloakUsersView.as_view()), path("accounts/", include("allauth.urls")), path("api-auth/", include("rest_framework.urls")), path("admin/", admin.site.urls), diff --git a/realm-export.json b/realm-export.json index d2036ac4f..8b48511a6 100644 --- a/realm-export.json +++ b/realm-export.json @@ -294,7 +294,7 @@ ], "security-admin-console": [], "admin-cli": [], - "test-client": [ + "gateway-client": [ { "id": "3f58b737-9ffa-455e-a36b-5a1b3f089080", "name": "uma_protection", @@ -374,6 +374,21 @@ "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, "webAuthnPolicyPasswordlessAcceptableAaguids": [], "users": [ + { + "username": "john", + "enabled": true, + "emailVerified": true, + "email": "john@example.com", + "firstName": "John", + "lastName": "Doe", + "credentials": [ + { + "type": "password", + "value": "password123", + "temporary": false + } + ] + }, { "id": "33b940e2-0bdb-49a7-9356-e6e230f49619", "createdTimestamp": 1640089861472, @@ -422,11 +437,11 @@ { "id": "83d84b8e-f053-480e-8b13-713c4fac708d", "createdTimestamp": 1640089810342, - "username": "service-account-test-client", + "username": "service-account-gateway-client", "enabled": true, "totp": false, "emailVerified": false, - "serviceAccountClientId": "test-client", + "serviceAccountClientId": "gateway-client", "disableableCredentialTypes": [], "requiredActions": [], "notBefore": 0, @@ -442,7 +457,7 @@ } ], "clientScopeMappings": { - "test-client": [ + "gateway-client": [ { "client": "admin-cli", "roles": [ @@ -844,14 +859,14 @@ }, { "id": "9a76b2ec-b33e-40b0-9cad-e00ca7e77e40", - "clientId": "test-client", + "clientId": "gateway-client", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "AQ3sZ4eiF7NhOtfxeUEGo0YN7uQBoUnO", "redirectUris": [ - "http://localhost:8081/callback" + "*" ], "webOrigins": [], "notBefore": 0, @@ -957,7 +972,7 @@ "resources": [ { "name": "Default Resource", - "type": "urn:test-client:resources:default", + "type": "urn:gateway-client:resources:default", "ownerManagedAccess": false, "attributes": {}, "_id": "c4c07a91-21b2-4259-b923-4b3d6b05d93f", @@ -986,7 +1001,7 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "defaultResourceType": "urn:test-client:resources:default", + "defaultResourceType": "urn:gateway-client:resources:default", "applyPolicies": "[\"Default Policy\"]" } } From 5a84d2e2dcc6a33c3cd67b4006bb3b6ec80f8289 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Fri, 17 Mar 2023 11:58:03 -0400 Subject: [PATCH 08/15] Issue 254 | Gateway + client: authentication and job logs --- client/quantum_serverless/core/constants.py | 3 + client/quantum_serverless/core/job.py | 14 ++++- client/quantum_serverless/core/provider.py | 65 +++++++++------------ gateway/api/apps.py | 3 + gateway/api/signals.py | 27 +++++++++ gateway/api/views.py | 7 +++ gateway/gateway/settings.py | 8 ++- 7 files changed, 85 insertions(+), 42 deletions(-) create mode 100644 gateway/api/signals.py diff --git a/client/quantum_serverless/core/constants.py b/client/quantum_serverless/core/constants.py index b2094bded..b13b27081 100644 --- a/client/quantum_serverless/core/constants.py +++ b/client/quantum_serverless/core/constants.py @@ -31,3 +31,6 @@ # request timeout REQUESTS_TIMEOUT: int = 30 + +# gateway +GATEWAY_PROVIDER_HOST = "GATEWAY_PROVIDER_HOST" diff --git a/client/quantum_serverless/core/job.py b/client/quantum_serverless/core/job.py index 7682c1f14..77a8a1e9f 100644 --- a/client/quantum_serverless/core/job.py +++ b/client/quantum_serverless/core/job.py @@ -149,7 +149,19 @@ def stop(self, job_id: str): raise NotImplementedError def logs(self, job_id: str): - raise NotImplementedError + result = None + response = requests.get( + f"{self.host}/jobs/{job_id}/logs/", + headers={"Authorization": f"Bearer {self._token}"}, + timeout=REQUESTS_TIMEOUT, + ) + if response.ok: + result = json.loads(response.text).get("logs", None) + else: + logging.warning( + "Something went wrong during job result fetching. %s", response.text + ) + return result def result(self, job_id: str): result = None diff --git a/client/quantum_serverless/core/provider.py b/client/quantum_serverless/core/provider.py index d597cc749..2bc3bbede 100644 --- a/client/quantum_serverless/core/provider.py +++ b/client/quantum_serverless/core/provider.py @@ -39,9 +39,8 @@ from quantum_serverless.core.constants import ( RAY_IMAGE, - ENV_KEYCLOAK_REALM, - ENV_KEYCLOAK_CLIENT_ID, REQUESTS_TIMEOUT, + GATEWAY_PROVIDER_HOST, ) from quantum_serverless.core.job import ( Job, @@ -474,13 +473,11 @@ class GatewayProvider(Provider): def __init__( self, - name: str, - host: str, - username: str, - password: str, - auth_host: Optional[str] = None, - realm: Optional[str] = None, - client_id: Optional[str] = None, + name: Optional[str] = None, + host: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + token: Optional[str] = None, ): """GatewayProvider. @@ -489,19 +486,26 @@ def __init__( host: host of gateway username: username password: password - auth_host: host of keycloak server - realm: keycloak realm - client_id: keycloak client id + token: authorization token """ + name = name or "gateway-provider" + + host = host or os.environ.get(GATEWAY_PROVIDER_HOST) + if host is None: + raise QuantumServerlessException("Please provide `host` of gateway.") + + if token is None and (username is None or password is None): + raise QuantumServerlessException( + "Authentication credentials must " + "be provided in form of `username` " + "and `password` or `token`." + ) + super().__init__(name) self.host = host - self.auth_host = auth_host or host - self._username = username - self._password = password - self._token = None - self._realm = realm or os.environ.get(ENV_KEYCLOAK_REALM, "Test") - self._client_id = client_id or os.environ.get(ENV_KEYCLOAK_CLIENT_ID, "newone") - self._fetch_token() + self._token = token + if token is None: + self._fetch_token(username, password) def get_compute_resources(self) -> List[ComputeResource]: raise NotImplementedError("GatewayProvider does not support resources api yet.") @@ -579,30 +583,15 @@ def get_jobs(self, **kwargs) -> List[Job]: return jobs - def _fetch_token(self): - keycloak_response = requests.post( - url=f"{self.auth_host}/auth/realms/{self._realm}/protocol/openid-connect/token", - data={ - "username": self._username, - "password": self._password, - "client_id": self._client_id, - "grant_type": "password", - }, - timeout=REQUESTS_TIMEOUT, - ) - if not keycloak_response.ok: - raise QuantumServerlessException("Incorrect credentials.") - - keycloak_token = json.loads(keycloak_response.text).get("access_token") - + def _fetch_token(self, username: str, password: str): gateway_response = requests.post( - url=f"{self.host}/dj-rest-auth/keycloak/", - data={"access_token": keycloak_token}, + url=f"{self.host}/dj-rest-auth/keycloak/login/", + data={"username": username, "password": password}, timeout=REQUESTS_TIMEOUT, ) if not gateway_response.ok: - raise QuantumServerlessException("Incorrect access token.") + raise QuantumServerlessException(gateway_response.text) gateway_token = json.loads(gateway_response.text).get("access_token") self._token = gateway_token diff --git a/gateway/api/apps.py b/gateway/api/apps.py index 1aaa98ad5..235deaa7c 100644 --- a/gateway/api/apps.py +++ b/gateway/api/apps.py @@ -8,3 +8,6 @@ class ApiConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "api" + + def ready(self): + import api.signals # pylint: disable=unused-import,import-outside-toplevel diff --git a/gateway/api/signals.py b/gateway/api/signals.py new file mode 100644 index 000000000..e158bfc94 --- /dev/null +++ b/gateway/api/signals.py @@ -0,0 +1,27 @@ +"""Api signals.""" +import logging + +from django.conf import settings +from django.db.models.signals import post_save +from django.dispatch import receiver + +from .models import ComputeResource + + +@receiver(post_save, sender=settings.AUTH_USER_MODEL) +def assign_compute_resource( + sender, instance, created, **kwargs # pylint: disable=unused-argument +): + """Assign default compute resource on user creation.""" + if created: + compute_resource = ComputeResource.objects.filter( + title="Ray cluster default" + ).first() + if compute_resource is not None: + compute_resource.users.add(instance) + compute_resource.save() + else: + logging.warning( + "ComputeResource was not found. " + "No compute resources will be assigned to newly created user." + ) diff --git a/gateway/api/views.py b/gateway/api/views.py index 0bffe50e2..944d7c97b 100644 --- a/gateway/api/views.py +++ b/gateway/api/views.py @@ -127,6 +127,13 @@ def result(self, request, pk=None): # pylint: disable=invalid-name,unused-argum job.save() return Response(JobSerializer(job).data) + @action(methods=["GET"], detail=True) + def logs(self, request, pk=None): # pylint: disable=invalid-name,unused-argument + """Returns logs from job.""" + job = self.get_object() + ray_client = JobSubmissionClient(job.compute_resource.host) + return Response({"logs": ray_client.get_job_logs(job.ray_job_id)}) + def ray_job_status_to_model_job_status(ray_job_status): """Maps ray job status to model job status.""" diff --git a/gateway/gateway/settings.py b/gateway/gateway/settings.py index 9573c2c51..6f3c85f3b 100644 --- a/gateway/gateway/settings.py +++ b/gateway/gateway/settings.py @@ -161,13 +161,15 @@ } SITE_ID = 1 -SITE_HOST = os.environ.get("SITE_HOST") +SITE_HOST = os.environ.get("SITE_HOST", "http://localhost:8000") # Provider specific settings SETTING_KEYCLOAK_URL = "SETTING_KEYCLOAK_URL" SETTING_KEYCLOAK_REALM = "SETTING_KEYCLOAK_REALM" -SETTINGS_KEYCLOAK_CLIENT_NAME = os.environ.get("CLIENT_ID") -SETTINGS_KEYCLOAK_CLIENT_SECRET = os.environ.get("SETTINGS_KEYCLOAK_CLIENT_SECRET") +SETTINGS_KEYCLOAK_CLIENT_NAME = os.environ.get("CLIENT_ID", "gateway-client") +SETTINGS_KEYCLOAK_CLIENT_SECRET = os.environ.get( + "SETTINGS_KEYCLOAK_CLIENT_SECRET", "AQ3sZ4eiF7NhOtfxeUEGo0YN7uQBoUnO" +) SETTINGS_KEYCLOAK_REQUESTS_TIMEOUT = int( os.environ.get("SETTINGS_KEYCLOAK_REQUESTS_TIMEOUT", 15) ) From e874180c090d9b3ecf66dfd34262af522050fece Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Fri, 17 Mar 2023 16:34:05 -0400 Subject: [PATCH 09/15] Issue 254 | Gateway: arguments and dependencies --- client/quantum_serverless/__init__.py | 1 + client/quantum_serverless/core/__init__.py | 1 + client/quantum_serverless/core/constants.py | 5 + client/quantum_serverless/core/job.py | 42 ++++++- client/quantum_serverless/core/provider.py | 21 ++-- docker-compose-dev.yml | 49 +++++++- docker-compose.yml | 18 +-- docs/guides/04_run_programs.ipynb | 105 ++++++++++++---- docs/guides/06_shared_state.ipynb | 32 +++-- docs/guides/program.py | 14 ++- docs/tutorials/01_quantum_serverless.ipynb | 112 ++++++++++++------ .../06_electronic_structure_problem.ipynb | 33 ++++-- docs/tutorials/07_benchmark_program.ipynb | 88 ++++++-------- gateway/.pylintrc | 2 +- ..._program_arguments_program_dependencies.py | 22 ++++ gateway/api/models.py | 3 + gateway/api/serializers.py | 2 +- gateway/api/utils.py | 12 ++ gateway/api/views.py | 48 ++++++-- 19 files changed, 441 insertions(+), 169 deletions(-) create mode 100644 gateway/api/migrations/0013_program_arguments_program_dependencies.py create mode 100644 gateway/api/utils.py diff --git a/client/quantum_serverless/__init__.py b/client/quantum_serverless/__init__.py index 11f15a1cb..f7ed202dc 100644 --- a/client/quantum_serverless/__init__.py +++ b/client/quantum_serverless/__init__.py @@ -22,6 +22,7 @@ get_refs_by_status, KuberayProvider, GatewayProvider, + save_result, ) from .quantum_serverless import QuantumServerless from .core.program import Program diff --git a/client/quantum_serverless/core/__init__.py b/client/quantum_serverless/core/__init__.py index 465b76ba5..102b110af 100644 --- a/client/quantum_serverless/core/__init__.py +++ b/client/quantum_serverless/core/__init__.py @@ -59,3 +59,4 @@ from .decorators import remote, get, put, run_qiskit_remote, get_refs_by_status from .events import RedisEventHandler, EventHandler, ExecutionMessage from .state import RedisStateHandler, StateHandler +from .job import save_result diff --git a/client/quantum_serverless/core/constants.py b/client/quantum_serverless/core/constants.py index b13b27081..82eae4827 100644 --- a/client/quantum_serverless/core/constants.py +++ b/client/quantum_serverless/core/constants.py @@ -34,3 +34,8 @@ # gateway GATEWAY_PROVIDER_HOST = "GATEWAY_PROVIDER_HOST" + +# auth +ENV_JOB_GATEWAY_TOKEN = "ENV_JOB_GATEWAY_TOKEN" +ENV_JOB_GATEWAY_HOST = "ENV_JOB_GATEWAY_HOST" +ENV_JOB_ID_GATEWAY = "ENV_JOB_ID_GATEWAY" diff --git a/client/quantum_serverless/core/job.py b/client/quantum_serverless/core/job.py index 77a8a1e9f..005b23b5e 100644 --- a/client/quantum_serverless/core/job.py +++ b/client/quantum_serverless/core/job.py @@ -29,14 +29,23 @@ """ import json import logging +import os +from typing import Dict, Any from uuid import uuid4 import ray.runtime_env import requests from ray.dashboard.modules.job.sdk import JobSubmissionClient -from quantum_serverless.core.constants import OT_PROGRAM_NAME, REQUESTS_TIMEOUT +from quantum_serverless.core.constants import ( + OT_PROGRAM_NAME, + REQUESTS_TIMEOUT, + ENV_JOB_GATEWAY_TOKEN, + ENV_JOB_GATEWAY_HOST, + ENV_JOB_ID_GATEWAY, +) from quantum_serverless.core.program import Program +from quantum_serverless.utils.json import is_jsonable RuntimeEnv = ray.runtime_env.RuntimeEnv @@ -210,3 +219,34 @@ def result(self): def __repr__(self): return f"" + + +def save_result(result: Dict[str, Any]): + """Saves job results.""" + token = os.environ.get(ENV_JOB_GATEWAY_TOKEN) + if token is None: + logging.warning( + "Results will not be saves as " + "there are no information about " + "authorization token in environment." + ) + return False + + if not is_jsonable(result): + logging.warning("Object passed is not json serializable.") + return False + + url = ( + f"{os.environ.get(ENV_JOB_GATEWAY_HOST)}/" + f"jobs/{os.environ.get(ENV_JOB_ID_GATEWAY)}/result/" + ) + response = requests.post( + url, + json={"result": result}, + headers={"Authorization": f"Bearer {token}"}, + timeout=REQUESTS_TIMEOUT, + ) + if not response.ok: + logging.warning("Something went wrong: %s", response.text) + + return response.ok diff --git a/client/quantum_serverless/core/provider.py b/client/quantum_serverless/core/provider.py index 2bc3bbede..5140f7403 100644 --- a/client/quantum_serverless/core/provider.py +++ b/client/quantum_serverless/core/provider.py @@ -537,14 +537,21 @@ def get_job_by_id(self, job_id: str) -> Optional[Job]: def run_program(self, program: Program) -> Job: url = f"{self.host}/programs/run_program/" - file_name = os.path.join(program.working_dir, "artifact.tar") - with tarfile.open(file_name, "w") as file: - file.add(program.working_dir) + artifact_file_path = os.path.join(program.working_dir, "artifact.tar") + with tarfile.open(artifact_file_path, "w") as tar: + for filename in os.listdir(program.working_dir): + fpath = os.path.join(program.working_dir, filename) + tar.add(fpath, arcname=filename) - with open(file_name, "rb") as file: + with open(artifact_file_path, "rb") as file: response = requests.post( url=url, - data={"title": program.name, "entrypoint": program.entrypoint}, + data={ + "title": program.name, + "entrypoint": program.entrypoint, + "arguments": json.dumps(program.arguments or {}), + "dependencies": json.dumps(program.dependencies or []), + }, files={"artifact": file}, headers={"Authorization": f"Bearer {self._token}"}, timeout=REQUESTS_TIMEOUT, @@ -557,8 +564,8 @@ def run_program(self, program: Program) -> Job: json_response = json.loads(response.text) job_id = json_response.get("id") - if os.path.exists(file_name): - os.remove(file_name) + if os.path.exists(artifact_file_path): + os.remove(artifact_file_path) return Job(job_id, job_client=GatewayJobClient(self.host, self._token)) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 779a61115..98741be26 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -51,6 +51,42 @@ services: - 9411:9411 networks: - safe-tier + postgres: + image: postgres + environment: + POSTGRES_DB: testkeycloakdb + POSTGRES_USER: testkeycloakuser + POSTGRES_PASSWORD: testkeycloakpassword + networks: + - safe-tier + restart: + always + keycloak: + container_name: keycloak + image: jboss/keycloak:16.1.0 + volumes: + - ./realm-export.json:/opt/jboss/keycloak/imports/realm-export.json + command: + - "-b 0.0.0.0 -Dkeycloak.profile.feature.upload_scripts=enabled -Dkeycloak.import=/opt/jboss/keycloak/imports/realm-export.json" + environment: + DB_VENDOR: POSTGRES + DB_ADDR: postgres + DB_DATABASE: testkeycloakdb + DB_USER: testkeycloakuser + DB_SCHEMA: public + DB_PASSWORD: testkeycloakpassword + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: 123 + PROXY_ADDRESS_FORWARDING: "true" + KEYCLOAK_LOGLEVEL: DEBUG + ports: + - '8085:8080' + depends_on: + - postgres + networks: + - safe-tier + restart: + always gateway: container_name: gateway build: ./gateway @@ -59,11 +95,18 @@ services: - 8000:8000 environment: - DEBUG=0 - - SITE_HOST=127.0.0.1 - - RAY_HOST=http://ray:8265 - - CLIENT_ID=newone + - RAY_HOST=http://ray-head:8265 + - CLIENT_ID=gateway-client - DJANGO_SUPERUSER_USERNAME=admin - DJANGO_SUPERUSER_PASSWORD=123 - DJANGO_SUPERUSER_EMAIL=admin@noemail.com + - SETTING_KEYCLOAK_URL=http://keycloak:8080/auth + - SETTING_KEYCLOAK_REALM=Test + - SETTINGS_KEYCLOAK_CLIENT_SECRET=AQ3sZ4eiF7NhOtfxeUEGo0YN7uQBoUnO + - SITE_HOST=http://gateway:8000 + networks: + - safe-tier + depends_on: + - keycloak networks: safe-tier: diff --git a/docker-compose.yml b/docker-compose.yml index 92ba42f32..d46c678f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,14 @@ # compose config on nightly builds services: -# jupyter: -# container_name: qs-jupyter -# image: qiskit/quantum-serverless-notebook:nightly-py39 -# ports: -# - 8888:8888 -# environment: -# - JUPYTER_TOKEN=123 -# networks: -# - safe-tier + jupyter: + container_name: qs-jupyter + image: qiskit/quantum-serverless-notebook:nightly-py39 + ports: + - 8888:8888 + environment: + - JUPYTER_TOKEN=123 + networks: + - safe-tier ray-head: container_name: ray-head image: qiskit/quantum-serverless-ray-node:nightly-py39 diff --git a/docs/guides/04_run_programs.ipynb b/docs/guides/04_run_programs.ipynb index 8145c2cbe..ab2c0b7ec 100644 --- a/docs/guides/04_run_programs.ipynb +++ b/docs/guides/04_run_programs.ipynb @@ -19,7 +19,8 @@ "metadata": {}, "outputs": [], "source": [ - "from quantum_serverless import QuantumServerless, run_qiskit_remote, get, Program" + "from quantum_serverless import QuantumServerless, Program, GatewayProvider, Provider\n", + "from quantum_serverless.core import ComputeResource" ] }, { @@ -31,7 +32,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 2, @@ -40,15 +41,13 @@ } ], "source": [ - "serverless = QuantumServerless({\n", - " \"providers\": [{\n", - " \"name\": \"docker\",\n", - " \"compute_resource\": {\n", - " \"name\": \"docker\",\n", - " \"host\": \"localhost\",\n", - " }\n", - " }]\n", - "})\n", + "gateway_provider = GatewayProvider(\n", + " username=\"john\",\n", + " password=\"password123\",\n", + " host=\"http://localhost:8000\",\n", + ")\n", + "\n", + "serverless = QuantumServerless([gateway_provider])\n", "serverless" ] }, @@ -95,17 +94,17 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 12, "id": "7612312e-91fc-4078-988b-4ffabfc9df63", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 7, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -133,17 +132,17 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 13, "id": "e67a039f-5a0f-415c-991a-926f3fbc0cbe", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "'SUCCEEDED'" ] }, - "execution_count": 15, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -154,28 +153,48 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 14, + "id": "3d22f21e-5d42-4917-800c-92de776b0b07", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Single execution: [0.]\n", + "N parallel executions: [array([0.]), array([1.]), array([0.])]\n", + "\n" + ] + } + ], + "source": [ + "print(job.logs())" + ] + }, + { + "cell_type": "code", + "execution_count": 15, "id": "4a2f2347-a3b2-45da-800a-95021aaddca1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'/home/ray/anaconda3/lib/python3.7/site-packages/ray/_private/worker.py:983: UserWarning: len(ctx) is deprecated. Use len(ctx.address_info) instead.\\n warnings.warn(\"len(ctx) is deprecated. Use len(ctx.address_info) instead.\")\\n[42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42]\\n'" + "'{\"single_execution\": [0.0], \"multiple_executions\": [[0.0], [1.0], [0.0]]}'" ] }, - "execution_count": 16, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "job.logs()" + "job.result()" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "id": "1cc48d5e-6c89-48ab-9845-46fd9dddacd5", "metadata": {}, "outputs": [], @@ -183,6 +202,48 @@ "# recover job by id\n", "recovered_job = serverless.get_job_by_id(job.job_id)" ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "1218d690-7f95-417d-a5ed-636c75638a0d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'SUCCEEDED'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recovered_job.status()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "d27c575a-29b6-46b4-919b-ed0507478e56", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"single_execution\": [0.0], \"multiple_executions\": [[0.0], [1.0], [0.0]]}'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recovered_job.result()" + ] } ], "metadata": { diff --git a/docs/guides/06_shared_state.ipynb b/docs/guides/06_shared_state.ipynb index f216cef1b..83d234afc 100644 --- a/docs/guides/06_shared_state.ipynb +++ b/docs/guides/06_shared_state.ipynb @@ -35,7 +35,7 @@ "metadata": {}, "outputs": [], "source": [ - "from quantum_serverless import QuantumServerless, Program\n", + "from quantum_serverless import QuantumServerless, Program, GatewayProvider\n", "from quantum_serverless.core.state import RedisStateHandler" ] }, @@ -56,7 +56,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 2, @@ -65,15 +65,13 @@ } ], "source": [ - "serverless = QuantumServerless({\n", - " \"providers\": [{\n", - " \"name\": \"docker\",\n", - " \"compute_resource\": {\n", - " \"name\": \"docker\",\n", - " \"host\": \"localhost\",\n", - " }\n", - " }]\n", - "})\n", + "gateway_provider = GatewayProvider(\n", + " username=\"john\",\n", + " password=\"password123\",\n", + " host=\"http://localhost:8000\",\n", + ")\n", + "\n", + "serverless = QuantumServerless(gateway_provider)\n", "serverless" ] }, @@ -144,7 +142,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -164,17 +162,17 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 9, "id": "6c9d41a6-cedf-4064-a1ea-fa9441c6ed9a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "'SUCCEEDED'" ] }, - "execution_count": 6, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -193,7 +191,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "id": "cb0acc2d-85c8-4850-a7ee-4af1f837b3fe", "metadata": {}, "outputs": [ @@ -203,7 +201,7 @@ "({'k': 42}, {'other_k': 42})" ] }, - "execution_count": 8, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } diff --git a/docs/guides/program.py b/docs/guides/program.py index 2c7d5d549..d68912e7f 100644 --- a/docs/guides/program.py +++ b/docs/guides/program.py @@ -3,7 +3,8 @@ from qiskit.quantum_info import SparsePauliOp from qiskit.primitives import Estimator -from quantum_serverless import QuantumServerless, run_qiskit_remote, get, put +from quantum_serverless import QuantumServerless, run_qiskit_remote, get, put, save_result + # 1. let's annotate out function to convert it # to function that can be executed remotely @@ -20,7 +21,7 @@ def my_function(circuit: QuantumCircuit, obs: SparsePauliOp): circuits = [random_circuit(2, 2) for _ in range(3)] # 3. create serverless context -with serverless: +with serverless.context(): # 4. let's put some shared objects into remote storage that will be shared among all executions obs_ref = put(SparsePauliOp(["ZZ"])) @@ -32,6 +33,13 @@ def my_function(circuit: QuantumCircuit, obs: SparsePauliOp): function_references = [my_function(circ, obs_ref) for circ in circuits] # 5. to get results back from reference - # we need to call `get` on function reference + # we need to call `get` on function reference print("Single execution:", get(function_reference)) print("N parallel executions:", get(function_references)) + + # 6. When saving results make sure objects + # passing as result are json serializable + save_result({ + "single_execution": get(function_reference).tolist(), + "multiple_executions": [entry.tolist() for entry in get(function_references)] + }) diff --git a/docs/tutorials/01_quantum_serverless.ipynb b/docs/tutorials/01_quantum_serverless.ipynb index 5471a4fc1..477d3f97e 100644 --- a/docs/tutorials/01_quantum_serverless.ipynb +++ b/docs/tutorials/01_quantum_serverless.ipynb @@ -219,25 +219,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 1, "id": "6277cbd3-a1ac-4a8f-8a29-c9c92732e780", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[EstimatorResult(values=array([1.]), metadata=[{}]), EstimatorResult(values=array([1.]), metadata=[{}]), EstimatorResult(values=array([1.]), metadata=[{}])]\n" - ] - } - ], + "outputs": [], "source": [ "from qiskit.circuit.random import random_circuit\n", "from qiskit.primitives import Estimator\n", "from qiskit.quantum_info import SparsePauliOp\n", "from quantum_serverless import QuantumServerless, run_qiskit_remote, put, get\n", "\n", - "serverless = QuantumServerless.load_configuration(\"./serverless_config.json\")\n", + "serverless = QuantumServerless()\n", "\n", "@run_qiskit_remote()\n", "def exp_val_remote(circuit, obs):\n", @@ -273,30 +265,29 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 3, "id": "75efe890-d7ea-4747-973c-b90e4df9fe39", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2023-01-09 10:42:24,138\tINFO dashboard_sdk.py:362 -- Package gcs://_ray_pkg_521b4aa7701fdb06.zip already exists, skipping upload.\n" - ] - }, { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 12, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "from quantum_serverless import Program\n", + "from quantum_serverless import Program, GatewayProvider\n", + "\n", + "gateway_provider = GatewayProvider(\n", + " username=\"john\",\n", + " password=\"password123\",\n", + " host=\"http://localhost:8000\",\n", + ")\n", "\n", "program = Program(\n", " name=\"brnchmark_program\",\n", @@ -305,23 +296,23 @@ " description=\"Benchmark program\"\n", ")\n", "\n", - "job = serverless.run_program(program)\n", + "job = serverless.set_provider(gateway_provider).run_program(program)\n", "job" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 4, "id": "0a7c52b4-c51e-4497-a013-5b1fdc9a5a79", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "'SUCCEEDED'" ] }, - "execution_count": 13, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -332,24 +323,73 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 6, "id": "f650750b-4b39-4932-b9dc-a51171d1d146", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'Execution time: 10.197080135345459\\nResults: [[EstimatorResult(values=array([-1.57009246e-16+6.35678836e-17j, 0.00000000e+00+2.00000000e+00j,\\n 1.00000000e+00+4.89879098e-01j, 0.00000000e+00-5.93914329e-01j,\\n 1.00000000e+00+3.84977936e-17j, -3.02888967e-01+0.00000000e+00j,\\n 9.23868961e-01+8.41944092e-01j, -1.00000000e+00-6.98797220e-17j,\\n 9.91094361e-01+5.68284953e-01j, -4.16333634e-17-1.00000000e+00j,\\n 8.07303767e-01+0.00000000e+00j, -5.34510053e-17+0.00000000e+00j,\\n -3.34083829e-01+1.00000000e+00j, 0.00000000e+00+1.65570683e+00j,\\n 8.87382321e-01+0.00000000e+00j, -8.81812854e-01-1.22464680e-16j,\\n -3.08751328e-01-5.78644041e-18j, 0.00000000e+00+1.92837761e-16j,\\n 0.00000000e+00+1.00000000e+00j, 2.00000000e+00+2.58341370e-16j,\\n -1.40395784e-16-8.97966034e-18j, 0.00000000e+00-8.65956056e-17j,\\n -1.22464680e-16-8.72832410e-34j, 1.00000000e+00+0.00000000e+00j,\\n 3.97958431e-01+6.42319385e-01j, -1.00000000e+00+1.00000000e+00j,\\n -1.66533454e-16+9.75159526e-01j, 0.00000000e+00+0.00000000e+00j,\\n -1.00000000e+00-4.83503087e-01j, -9.52623642e-01-5.95841695e-02j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([-1.57009246e-16+6.35678836e-17j, 0.00000000e+00+2.00000000e+00j,\\n 1.00000000e+00+4.89879098e-01j, 0.00000000e+00-5.93914329e-01j,\\n 1.00000000e+00+3.84977936e-17j, -3.02888967e-01+0.00000000e+00j,\\n 9.23868961e-01+8.41944092e-01j, -1.00000000e+00-6.98797220e-17j,\\n 9.91094361e-01+5.68284953e-01j, -4.16333634e-17-1.00000000e+00j,\\n 8.07303767e-01+0.00000000e+00j, -5.34510053e-17+0.00000000e+00j,\\n -3.34083829e-01+1.00000000e+00j, 0.00000000e+00+1.65570683e+00j,\\n 8.87382321e-01+0.00000000e+00j, -8.81812854e-01-1.22464680e-16j,\\n -3.08751328e-01-5.78644041e-18j, 0.00000000e+00+1.92837761e-16j,\\n 0.00000000e+00+1.00000000e+00j, 2.00000000e+00+2.58341370e-16j,\\n -1.40395784e-16-8.97966034e-18j, 0.00000000e+00-8.65956056e-17j,\\n -1.22464680e-16-8.72832410e-34j, 1.00000000e+00+0.00000000e+00j,\\n 3.97958431e-01+6.42319385e-01j, -1.00000000e+00+1.00000000e+00j,\\n -1.66533454e-16+9.75159526e-01j, 0.00000000e+00+0.00000000e+00j,\\n -1.00000000e+00-4.83503087e-01j, -9.52623642e-01-5.95841695e-02j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([-1.57009246e-16+6.35678836e-17j, 0.00000000e+00+2.00000000e+00j,\\n 1.00000000e+00+4.89879098e-01j, 0.00000000e+00-5.93914329e-01j,\\n 1.00000000e+00+3.84977936e-17j, -3.02888967e-01+0.00000000e+00j,\\n 9.23868961e-01+8.41944092e-01j, -1.00000000e+00-6.98797220e-17j,\\n 9.91094361e-01+5.68284953e-01j, -4.16333634e-17-1.00000000e+00j,\\n 8.07303767e-01+0.00000000e+00j, -5.34510053e-17+0.00000000e+00j,\\n -3.34083829e-01+1.00000000e+00j, 0.00000000e+00+1.65570683e+00j,\\n 8.87382321e-01+0.00000000e+00j, -8.81812854e-01-1.22464680e-16j,\\n -3.08751328e-01-5.78644041e-18j, 0.00000000e+00+1.92837761e-16j,\\n 0.00000000e+00+1.00000000e+00j, 2.00000000e+00+2.58341370e-16j,\\n -1.40395784e-16-8.97966034e-18j, 0.00000000e+00-8.65956056e-17j,\\n -1.22464680e-16-8.72832410e-34j, 1.00000000e+00+0.00000000e+00j,\\n 3.97958431e-01+6.42319385e-01j, -1.00000000e+00+1.00000000e+00j,\\n -1.66533454e-16+9.75159526e-01j, 0.00000000e+00+0.00000000e+00j,\\n -1.00000000e+00-4.83503087e-01j, -9.52623642e-01-5.95841695e-02j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}])]]\\n/home/ray/anaconda3/lib/python3.7/site-packages/ray/_private/worker.py:983: UserWarning: len(ctx) is deprecated. Use len(ctx.address_info) instead.\\n warnings.warn(\"len(ctx) is deprecated. Use len(ctx.address_info) instead.\")\\n'" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Execution time: 9.168829679489136\n", + "Results: [[EstimatorResult(values=array([-4.04804878e-01+6.73804871e-01j, -1.94289029e-16-8.76563346e-01j,\n", + " 0.00000000e+00+2.93954499e-16j, 0.00000000e+00+1.00000000e+00j,\n", + " -5.78736907e-01-2.02739106e-01j, -8.32667268e-17-1.00000000e+00j,\n", + " -4.89359541e-16-1.15784406e-16j, 0.00000000e+00+3.87904546e-16j,\n", + " -5.34438212e-01+5.55111512e-17j, -3.51585782e-17+1.06106206e+00j,\n", + " -2.49262374e-02-2.61221914e-16j, 1.00000000e+00+1.00000000e+00j,\n", + " -4.49088083e-01+0.00000000e+00j, -1.00000000e+00+5.73423770e-17j,\n", + " -1.00000000e+00+4.90611306e-01j, -3.14018492e-16+9.81307787e-17j,\n", + " 1.81756717e-01-3.15451888e-02j, -1.33434861e+00-1.11022302e-16j,\n", + " 6.02950130e-02+1.56597418e-02j, -4.34512241e-01-3.34194313e-01j,\n", + " 0.00000000e+00+1.00000000e+00j, 0.00000000e+00-1.00000000e+00j,\n", + " 9.56512057e-01-2.30245304e-01j, -6.87854373e-01-7.04747198e-01j,\n", + " -8.67447001e-01-2.58061563e-16j, -1.74208517e-16+1.69868804e-16j,\n", + " -8.30268506e-01+0.00000000e+00j, 5.28163400e-01-5.55111512e-17j,\n", + " 9.06271517e-01-1.00000000e+00j, 1.69275103e-01+8.24470603e-01j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([-4.04804878e-01+6.73804871e-01j, -1.94289029e-16-8.76563346e-01j,\n", + " 0.00000000e+00+2.93954499e-16j, 0.00000000e+00+1.00000000e+00j,\n", + " -5.78736907e-01-2.02739106e-01j, -8.32667268e-17-1.00000000e+00j,\n", + " -4.89359541e-16-1.15784406e-16j, 0.00000000e+00+3.87904546e-16j,\n", + " -5.34438212e-01+5.55111512e-17j, -3.51585782e-17+1.06106206e+00j,\n", + " -2.49262374e-02-2.61221914e-16j, 1.00000000e+00+1.00000000e+00j,\n", + " -4.49088083e-01+0.00000000e+00j, -1.00000000e+00+5.73423770e-17j,\n", + " -1.00000000e+00+4.90611306e-01j, -3.14018492e-16+9.81307787e-17j,\n", + " 1.81756717e-01-3.15451888e-02j, -1.33434861e+00-1.11022302e-16j,\n", + " 6.02950130e-02+1.56597418e-02j, -4.34512241e-01-3.34194313e-01j,\n", + " 0.00000000e+00+1.00000000e+00j, 0.00000000e+00-1.00000000e+00j,\n", + " 9.56512057e-01-2.30245304e-01j, -6.87854373e-01-7.04747198e-01j,\n", + " -8.67447001e-01-2.58061563e-16j, -1.74208517e-16+1.69868804e-16j,\n", + " -8.30268506e-01+0.00000000e+00j, 5.28163400e-01-5.55111512e-17j,\n", + " 9.06271517e-01-1.00000000e+00j, 1.69275103e-01+8.24470603e-01j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([-4.04804878e-01+6.73804871e-01j, -1.94289029e-16-8.76563346e-01j,\n", + " 0.00000000e+00+2.93954499e-16j, 0.00000000e+00+1.00000000e+00j,\n", + " -5.78736907e-01-2.02739106e-01j, -8.32667268e-17-1.00000000e+00j,\n", + " -4.89359541e-16-1.15784406e-16j, 0.00000000e+00+3.87904546e-16j,\n", + " -5.34438212e-01+5.55111512e-17j, -3.51585782e-17+1.06106206e+00j,\n", + " -2.49262374e-02-2.61221914e-16j, 1.00000000e+00+1.00000000e+00j,\n", + " -4.49088083e-01+0.00000000e+00j, -1.00000000e+00+5.73423770e-17j,\n", + " -1.00000000e+00+4.90611306e-01j, -3.14018492e-16+9.81307787e-17j,\n", + " 1.81756717e-01-3.15451888e-02j, -1.33434861e+00-1.11022302e-16j,\n", + " 6.02950130e-02+1.56597418e-02j, -4.34512241e-01-3.34194313e-01j,\n", + " 0.00000000e+00+1.00000000e+00j, 0.00000000e+00-1.00000000e+00j,\n", + " 9.56512057e-01-2.30245304e-01j, -6.87854373e-01-7.04747198e-01j,\n", + " -8.67447001e-01-2.58061563e-16j, -1.74208517e-16+1.69868804e-16j,\n", + " -8.30268506e-01+0.00000000e+00j, 5.28163400e-01-5.55111512e-17j,\n", + " 9.06271517e-01-1.00000000e+00j, 1.69275103e-01+8.24470603e-01j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}])]]\n", + "\n" + ] } ], "source": [ - "job.logs()" + "print(job.logs())" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b65df46e-303c-48f5-9638-8c7d11e7c828", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/docs/tutorials/06_electronic_structure_problem.ipynb b/docs/tutorials/06_electronic_structure_problem.ipynb index 3ee4624ad..8f75b2d6e 100644 --- a/docs/tutorials/06_electronic_structure_problem.ipynb +++ b/docs/tutorials/06_electronic_structure_problem.ipynb @@ -102,41 +102,54 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 1, "id": "290bf01e-3175-4c60-b7b7-ce02ea40fb08", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", - "from quantum_serverless import QuantumServerless, Program\n", + "from quantum_serverless import QuantumServerless, Program, GatewayProvider\n", "from quantum_serverless.core import RedisStateHandler" ] }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 4, "id": "c35bf9df-e68c-45b7-a992-2471aee29d60", "metadata": {}, "outputs": [], "source": [ - "serverless = QuantumServerless.load_configuration(\"./serverless_config.json\") # config with local docker-compose setup\n", + "gateway_provider = GatewayProvider(\n", + " username=\"john\",\n", + " password=\"password123\",\n", + " host=\"http://localhost:8000\",\n", + ")\n", + "\n", + "serverless = QuantumServerless(gateway_provider) # config with local docker-compose setup\n", "state_handler = RedisStateHandler(\"localhost\", 6379) # creating state handler to read and write data from jobs" ] }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 8, "id": "a0789a71-0b84-4736-9f34-579205dbc3c2", "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{} [\"pyscf==1.6.3\"]\n" + ] + }, { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 70, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -156,7 +169,7 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 10, "id": "0d0c3e32-ea08-4939-a4ca-6b6150f2c2c8", "metadata": {}, "outputs": [ @@ -165,9 +178,7 @@ "output_type": "stream", "text": [ "Logs:\n", - "Energies: [-1.1696350782475755, -1.0949469830238987, -0.9811713427333931]\n", - "Shifts: [-6.609784771130337, -6.781951626949085, -6.870414678425508]\n", - "Energy + shift: [-7.779419849377913, -7.876898609972984, -7.8515860211589015]\n" + "\n" ] } ], diff --git a/docs/tutorials/07_benchmark_program.ipynb b/docs/tutorials/07_benchmark_program.ipynb index 8cb95b7f7..2bb63422c 100644 --- a/docs/tutorials/07_benchmark_program.ipynb +++ b/docs/tutorials/07_benchmark_program.ipynb @@ -31,57 +31,61 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "b031e9c4-8503-4535-8517-22f445593eb5", "metadata": {}, "outputs": [], "source": [ - "from quantum_serverless import QuantumServerless, Program" + "from quantum_serverless import QuantumServerless, Program, GatewayProvider" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "3694d35b-6ad7-4a60-9edc-9c674be56908", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 4, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "serverless = QuantumServerless({\n", - " \"providers\": [{\n", - " \"name\": \"docker\",\n", - " \"compute_resource\": {\n", - " \"name\": \"docker\",\n", - " \"host\": \"localhost\",\n", - " }\n", - " }]\n", - "})\n", + "gateway_provider = GatewayProvider(\n", + " username=\"john\",\n", + " password=\"password123\",\n", + " host=\"http://localhost:8000\",\n", + ")\n", + "serverless = QuantumServerless(gateway_provider)\n", "serverless" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "030ccde4-d18b-43fc-979e-68df797f5977", "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"n_qubits\": 2, \"n_entries\": 2, \"depth_of_recursion\": 4, \"n_backends\": 3, \"n_graphs\": 1} []\n" + ] + }, { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 5, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -107,45 +111,31 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 5, "id": "316d3bdd-c9ed-404e-bf76-afca850920f5", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "['Execution time: 18.622056245803833',\n", - " 'Results: [[EstimatorResult(values=array([ 1.00000000e+00-1.00000000e+00j, 2.22044605e-16-1.00000000e+00j,',\n", - " ' 2.16726672e-16+8.65956056e-17j, 0.00000000e+00+9.99342407e-01j,',\n", - " ' -1.00000000e+00+2.44929360e-16j, -4.41586599e-01-7.07106781e-01j,',\n", - " ' -5.01468367e-16+1.00000000e+00j, -3.92943373e-17-1.20799277e-16j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([ 1.00000000e+00-1.00000000e+00j, 2.22044605e-16-1.00000000e+00j,',\n", - " ' 2.16726672e-16+8.65956056e-17j, 0.00000000e+00+9.99342407e-01j,',\n", - " ' -1.00000000e+00+2.44929360e-16j, -4.41586599e-01-7.07106781e-01j,',\n", - " ' -5.01468367e-16+1.00000000e+00j, -3.92943373e-17-1.20799277e-16j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([ 1.00000000e+00-1.00000000e+00j, 2.22044605e-16-1.00000000e+00j,',\n", - " ' 2.16726672e-16+8.65956056e-17j, 0.00000000e+00+9.99342407e-01j,',\n", - " ' -1.00000000e+00+2.44929360e-16j, -4.41586599e-01-7.07106781e-01j,',\n", - " ' -5.01468367e-16+1.00000000e+00j, -3.92943373e-17-1.20799277e-16j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}])], [EstimatorResult(values=array([-4.18409669e-02-9.92346029e-02j, -5.55111512e-17-1.70198288e-01j,',\n", - " ' 1.00000000e+00+0.00000000e+00j, -8.94166369e-01+3.16884478e-01j,',\n", - " ' -6.55602185e-01+0.00000000e+00j, 3.86457683e-18-1.00000000e+00j,',\n", - " ' -1.00000000e+00-1.22464680e-16j, 0.00000000e+00+1.01968358e-17j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([-4.18409669e-02-9.92346029e-02j, -5.55111512e-17-1.70198288e-01j,',\n", - " ' 1.00000000e+00+0.00000000e+00j, -8.94166369e-01+3.16884478e-01j,',\n", - " ' -6.55602185e-01+0.00000000e+00j, 3.86457683e-18-1.00000000e+00j,',\n", - " ' -1.00000000e+00-1.22464680e-16j, 0.00000000e+00+1.01968358e-17j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([-4.18409669e-02-9.92346029e-02j, -5.55111512e-17-1.70198288e-01j,',\n", - " ' 1.00000000e+00+0.00000000e+00j, -8.94166369e-01+3.16884478e-01j,',\n", - " ' -6.55602185e-01+0.00000000e+00j, 3.86457683e-18-1.00000000e+00j,',\n", - " ' -1.00000000e+00-1.22464680e-16j, 0.00000000e+00+1.01968358e-17j]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}])]]',\n", - " '/home/ray/anaconda3/lib/python3.7/site-packages/ray/_private/worker.py:983: UserWarning: len(ctx) is deprecated. Use len(ctx.address_info) instead.',\n", - " ' warnings.warn(\"len(ctx) is deprecated. Use len(ctx.address_info) instead.\")',\n", - " '']" - ] - }, - "execution_count": 56, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Execution time: 9.523066759109497\n", + "Results: [[EstimatorResult(values=array([ 3.30466138e-01+0.97886459j, 6.99025164e-17+1.j ,\n", + " 3.59667555e-01+0.07793376j, 9.59608666e-01-0.95960867j,\n", + " -9.20956756e-01-0.99668204j, 0.00000000e+00-1.j ,\n", + " 0.00000000e+00+0.138121j , 2.55847956e-01+0.j ]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([ 3.30466138e-01+0.97886459j, 6.99025164e-17+1.j ,\n", + " 3.59667555e-01+0.07793376j, 9.59608666e-01-0.95960867j,\n", + " -9.20956756e-01-0.99668204j, 0.00000000e+00-1.j ,\n", + " 0.00000000e+00+0.138121j , 2.55847956e-01+0.j ]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}]), EstimatorResult(values=array([ 3.30466138e-01+0.97886459j, 6.99025164e-17+1.j ,\n", + " 3.59667555e-01+0.07793376j, 9.59608666e-01-0.95960867j,\n", + " -9.20956756e-01-0.99668204j, 0.00000000e+00-1.j ,\n", + " 0.00000000e+00+0.138121j , 2.55847956e-01+0.j ]), metadata=[{}, {}, {}, {}, {}, {}, {}, {}])]]\n", + "\n" + ] } ], "source": [ - "job.logs().split(\"\\n\")" + "print(job.logs())" ] } ], diff --git a/gateway/.pylintrc b/gateway/.pylintrc index 44a0fc58e..1efe2a509 100644 --- a/gateway/.pylintrc +++ b/gateway/.pylintrc @@ -526,7 +526,7 @@ max-bool-expr=5 max-branches=12 # Maximum number of locals for function / method body. -max-locals=15 +max-locals=20 # Maximum number of parents for a class (see R0901). max-parents=7 diff --git a/gateway/api/migrations/0013_program_arguments_program_dependencies.py b/gateway/api/migrations/0013_program_arguments_program_dependencies.py new file mode 100644 index 000000000..9dff4c00a --- /dev/null +++ b/gateway/api/migrations/0013_program_arguments_program_dependencies.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1 on 2023-03-17 19:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0012_alter_program_artifact"), + ] + + operations = [ + migrations.AddField( + model_name="program", + name="arguments", + field=models.TextField(blank=True, default="{}"), + ), + migrations.AddField( + model_name="program", + name="dependencies", + field=models.TextField(blank=True, default="[]"), + ), + ] diff --git a/gateway/api/models.py b/gateway/api/models.py index f31395d2d..cdc64ff49 100644 --- a/gateway/api/models.py +++ b/gateway/api/models.py @@ -20,6 +20,9 @@ class Program(models.Model): on_delete=models.CASCADE, ) + arguments = models.TextField(null=False, blank=True, default="{}") + dependencies = models.TextField(null=False, blank=True, default="[]") + def __str__(self): return f"{self.title}" diff --git a/gateway/api/serializers.py b/gateway/api/serializers.py index a7573a00a..3b54c3c4b 100644 --- a/gateway/api/serializers.py +++ b/gateway/api/serializers.py @@ -10,7 +10,7 @@ class ProgramSerializer(serializers.ModelSerializer): class Meta: model = Program - fields = ["title", "entrypoint", "artifact"] + fields = ["title", "entrypoint", "artifact", "dependencies", "arguments"] class JobSerializer(serializers.ModelSerializer): diff --git a/gateway/api/utils.py b/gateway/api/utils.py new file mode 100644 index 000000000..c81623b1c --- /dev/null +++ b/gateway/api/utils.py @@ -0,0 +1,12 @@ +"""Utilities.""" +import json +from typing import Optional, Tuple + + +def try_json_loads(data: str) -> Tuple[bool, Optional[dict]]: + """Dumb check if data is json :)""" + try: + json_object = json.loads(data) + except ValueError: + return False, None + return True, json_object diff --git a/gateway/api/views.py b/gateway/api/views.py index 944d7c97b..9a955665d 100644 --- a/gateway/api/views.py +++ b/gateway/api/views.py @@ -22,6 +22,7 @@ from .models import Program, Job, ComputeResource from .permissions import IsOwner from .serializers import ProgramSerializer, JobSerializer +from .utils import try_json_loads # pylint: disable=too-many-ancestors @@ -46,13 +47,18 @@ def run_program(self, request): if serializer.is_valid(): # create program program = Program(**serializer.data) + _, dependencies = try_json_loads(program.dependencies) + _, arguments = try_json_loads(program.arguments) existing_programs = Program.objects.filter( author=request.user, title__exact=program.title ) if existing_programs.count() > 0: # take existing one - program = existing_programs.first() + existing_programs = existing_programs.first() + existing_programs.arguments = program.arguments + existing_programs.dependencies = program.dependencies + program = existing_programs program.artifact = request.FILES.get("artifact") program.author = request.user program.save() @@ -74,21 +80,45 @@ def run_program(self, request): settings.MEDIA_ROOT, "tmp", str(uuid.uuid4()) ) file.extractall(extract_folder) - ray_job_id = ray_client.submit_job( - entrypoint=f"python {program.entrypoint}", - runtime_env={"working_dir": extract_folder}, - ) - # remote temp data - if os.path.exists(extract_folder): - shutil.rmtree(extract_folder) job = Job( program=program, author=request.user, - ray_job_id=ray_job_id, compute_resource=compute_resource, ) job.save() + + if arguments is not None: + arg_list = [] + for key, value in arguments.items(): + if isinstance(value, dict): + arg_list.append(f"--{key}='{json.dumps(value)}'") + else: + arg_list.append(f"--{key}={value}") + arguments = " ".join(arg_list) + else: + arguments = "" + entrypoint = f"python {program.entrypoint} {arguments}" + + ray_job_id = ray_client.submit_job( + entrypoint=entrypoint, + runtime_env={ + "working_dir": extract_folder, + "env_vars": { + "ENV_JOB_GATEWAY_TOKEN": str(request.auth.token.decode()), + "ENV_JOB_GATEWAY_HOST": str(settings.SITE_HOST), + "ENV_JOB_ID_GATEWAY": str(job.id), + }, + "pip": dependencies or [], + }, + ) + job.ray_job_id = ray_job_id + job.save() + + # remote temp data + if os.path.exists(extract_folder): + shutil.rmtree(extract_folder) + return Response(JobSerializer(job).data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 2fdadf76d13dedbbf9869df367af58215e56a278 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Fri, 17 Mar 2023 18:08:42 -0400 Subject: [PATCH 10/15] Issue 254 | Client: switch name to title --- client/quantum_serverless/core/job.py | 2 +- client/quantum_serverless/core/provider.py | 2 +- client/tests/core/test_decorator.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/quantum_serverless/core/job.py b/client/quantum_serverless/core/job.py index 005b23b5e..8dd4878f4 100644 --- a/client/quantum_serverless/core/job.py +++ b/client/quantum_serverless/core/job.py @@ -111,7 +111,7 @@ def run_program(self, program: Program): entrypoint = f"python {program.entrypoint} {arguments}" # set program name so OT can use it as parent span name - env_vars = {**(program.env_vars or {}), **{OT_PROGRAM_NAME: program.name}} + env_vars = {**(program.env_vars or {}), **{OT_PROGRAM_NAME: program.title}} job_id = self._job_client.submit_job( entrypoint=entrypoint, diff --git a/client/quantum_serverless/core/provider.py b/client/quantum_serverless/core/provider.py index 5140f7403..065bc5374 100644 --- a/client/quantum_serverless/core/provider.py +++ b/client/quantum_serverless/core/provider.py @@ -547,7 +547,7 @@ def run_program(self, program: Program) -> Job: response = requests.post( url=url, data={ - "title": program.name, + "title": program.title, "entrypoint": program.entrypoint, "arguments": json.dumps(program.arguments or {}), "dependencies": json.dumps(program.dependencies or []), diff --git a/client/tests/core/test_decorator.py b/client/tests/core/test_decorator.py index d5cb25a3a..063b2c249 100644 --- a/client/tests/core/test_decorator.py +++ b/client/tests/core/test_decorator.py @@ -65,7 +65,7 @@ def ultimate_function(ultimate_argument: int): ) return mid_result - with serverless: + with serverless.context(): reference = ultimate_function(1) result = get(reference) self.assertEqual(result, 4) @@ -87,7 +87,7 @@ def ultimate_function_with_state(state: StateHandler, ultimate_argument: int): state.set("some_key", {"result": ultimate_argument}) return state.get("some_key") - with serverless: + with serverless.context(): reference = ( ultimate_function_with_state( # pylint: disable=no-value-for-parameter 1 From bac0af7e15e9008d7a9846c372cf5767f8a2a5f5 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Fri, 17 Mar 2023 20:22:01 -0400 Subject: [PATCH 11/15] Issue 217 | Docs: resolve comments --- docs/Untitled.ipynb | 92 ++++++++ docs/getting_started/01_intro_level_1.ipynb | 76 +++--- docs/getting_started/01_intro_level_2.ipynb | 217 +++++++++++++++--- docs/getting_started/01_intro_level_3.ipynb | 128 ++++------- .../source_files/gs_level_3.py | 28 ++- 5 files changed, 383 insertions(+), 158 deletions(-) create mode 100644 docs/Untitled.ipynb diff --git a/docs/Untitled.ipynb b/docs/Untitled.ipynb new file mode 100644 index 000000000..a14cd8fec --- /dev/null +++ b/docs/Untitled.ipynb @@ -0,0 +1,92 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "7c048ff1-5bed-4972-900b-4cda6ac4aeed", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import IBMQ" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "bcf2a92a-3aef-42bf-990f-528964cd33c2", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "ibmqfactory.load_account:WARNING:2023-03-17 18:27:17,504: Credentials are already in use. The existing account in the session will be replaced.\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IBMQ.load_account() # Load account from disk" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "193e6835-a158-4b56-a36e-91154e36a77d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IBMQ.get_provider(group=\"quantum-meeting\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28c2dcce-b2b9-426b-b1c6-dc01cf3fc9e0", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/getting_started/01_intro_level_1.ipynb b/docs/getting_started/01_intro_level_1.ipynb index 2c893b49a..70313681d 100644 --- a/docs/getting_started/01_intro_level_1.ipynb +++ b/docs/getting_started/01_intro_level_1.ipynb @@ -9,7 +9,7 @@ "\n", "Let's write `Hello World` program using quantum serverless. \n", "\n", - "We will start with writing code for our program and saving it to `./source_files/gs_level_1.py` file. It will be simple hello world qiskit example.\n", + "We will start with writing code for our program and saving it to [./source_files/gs_level_1.py](./source_files/gs_level_1.py) file. It will be simple hello world qiskit example.\n", "\n", "```python\n", "# source_files/gs_level_1.py\n", @@ -30,7 +30,11 @@ "print(f\"Quasi distribution: {quasi_dists[0]}\")\n", "```\n", "\n", - "Next we need to run this program. For that we need to import necessary modules and configure QuantumServerless client. We are doing so by providing name and host for deployed infrastructure." + "Next we need to run this program. For that we need to import necessary classes and configure them. \n", + "One of those classes is `QuantumServerless`, which is a client class to interact with compute resources.\n", + "It will help us run programs, monitor progress and fetch results.\n", + "\n", + "`QuantumServerless` accepts `Provider` as a constructor argument. Provider stores configuration where our compute resources are and how to connect to them. For this example we will be using provider which is connected to local docker-compose setup. For more information on docker-compose check out [docker docs](https://docs.docker.com/compose/), but for now you can think of it as your local environment manager. So, in this example programs will be running locally on your machine. If you want to run it elsewhere, you need to provide corresponding host and authentication details." ] }, { @@ -40,36 +44,34 @@ "metadata": {}, "outputs": [], "source": [ - "from quantum_serverless import QuantumServerless, Program" + "from quantum_serverless import QuantumServerless, GatewayProvider" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "acdec789-4967-48ee-8f6c-8d2b0ff57e91", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "serverless = QuantumServerless({\n", - " \"providers\": [{\n", - " \"name\": \"docker\",\n", - " \"compute_resource\": {\n", - " \"name\": \"docker\",\n", - " \"host\": \"localhost\",\n", - " }\n", - " }]\n", - "})\n", + "provider = GatewayProvider(\n", + " username=\"john\", # username is predefined in local docker setup\n", + " password=\"password123\", # password is predefined in local docker setup\n", + " host=\"http://localhost:8000\", # address of provider\n", + ")\n", + "\n", + "serverless = QuantumServerless(provider)\n", "serverless" ] }, @@ -88,26 +90,28 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "id": "d51df836-3f22-467c-b637-5803145d5d8a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 3, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "from quantum_serverless import Program\n", + "\n", "program = Program(\n", - " name=\"Getting started program level 1\",\n", - " entrypoint=\"gs_level_1.py\",\n", - " working_dir=\"./source_files/\"\n", + " title=\"Getting started program level 1\", # you can choose any name you like. It is used to deferentiate if you have a lot of programs in array.\n", + " entrypoint=\"gs_level_1.py\", # entrypoint is file that will start your calculation\n", + " working_dir=\"./source_files/\" # where you files are located. By default it is current directory.\n", ")\n", "\n", "job = serverless.run_program(program)\n", @@ -124,17 +128,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "cc7ccea6-bbae-4184-ba7f-67b6c20a0b0b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "'SUCCEEDED'" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -145,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "ca76abfa-2ff5-425b-a225-058d91348e8b", "metadata": {}, "outputs": [ @@ -155,7 +159,7 @@ "'Quasi distribution: {0: 0.4999999999999999, 3: 0.4999999999999999}\\n'" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -175,17 +179,17 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "f942b76d-596c-4384-8f36-e5f73e72cefd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'qs_76b8db40-6150-48bf-b971-ac0b13db12b5'" + "1" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -194,19 +198,27 @@ "job.job_id" ] }, + { + "cell_type": "markdown", + "id": "a92069ba-0a3c-4c9f-8e8d-3916a2eb2093", + "metadata": {}, + "source": [ + "Users can fetch previously ran jobs from configured providers." + ] + }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "45e2927f-655b-47a4-8003-f16e5ba0a1cd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } diff --git a/docs/getting_started/01_intro_level_2.ipynb b/docs/getting_started/01_intro_level_2.ipynb index ccbae9b24..2b112eff2 100644 --- a/docs/getting_started/01_intro_level_2.ipynb +++ b/docs/getting_started/01_intro_level_2.ipynb @@ -9,7 +9,7 @@ "\n", "In this tutorial we will explore a little bit more advanced example of a program that require some configuration, requirements setup, etc. \n", "\n", - "Again we will start with writing code for our program and saving it to `./source_files/gs_level_2.py` file.\n", + "Again we will start with writing code for our program and saving it to [./source_files/gs_level_2.py](./source_files/gs_level_2.py) file.\n", "This time it will be VQE example from [Qiskit documentation](https://qiskit.org/documentation/nature/tutorials/07_leveraging_qiskit_runtime.html) and we also introduce dependency management and arguments to our programs.\n", "\n", "```python\n", @@ -89,76 +89,83 @@ "\n", "As you can see here we used couple of additional things compared to `getting started level 1`. \n", "\n", - "First we are using `qiskit-nature` module and `pyscf` extension. \n", - "We also using argument parsing to accept arguments to our program. In this case argument is `bond_length`. \n", + "First, we are introducing dependency management by using the `qiskit-nature` module and `pyscf` extension.\n", + "We also using argument parsing to accept arguments to our program. In this case argument is `bond_length`. This means that we can, re-run our program over different bond lengths and produce a dissociation curve.\n", "\n", - "Next we need to run this program. For that we need to import necessary modules and configure `QuantumServerless` client. We are doing so by providing name and host for deployed infrastructure.\n", "\n", - "In addition to that we will provide additional `dependencies` and `arguments` to our `Program` construction.\n", - "- `dependencies` parameter will install provided libraries to run our script\n", - "- `arguments` parameter is a dictionary with arguments that will be passed for script execution" + "Next we need to run this program. For that we need to import necessary modules and configure `QuantumServerless` client. We are doing so by providing name and host for deployed infrastructure." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "id": "79434a17-1222-4d04-a81a-8140ed630ed6", "metadata": {}, "outputs": [], "source": [ - "from quantum_serverless import QuantumServerless, Program" + "from quantum_serverless import QuantumServerless, GatewayProvider" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "id": "b6ec8969-8c3d-4b7f-8c4c-adc6dbb9c59f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 2, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "serverless = QuantumServerless({\n", - " \"providers\": [{\n", - " \"name\": \"docker\",\n", - " \"compute_resource\": {\n", - " \"name\": \"docker\",\n", - " \"host\": \"localhost\",\n", - " }\n", - " }]\n", - "})\n", + "provider = GatewayProvider(\n", + " username=\"john\", # username is predefined in local docker setup\n", + " password=\"password123\", # password is predefined in local docker setup\n", + " host=\"http://localhost:8000\", # address of provider\n", + ")\n", + "\n", + "serverless = QuantumServerless(provider)\n", "serverless" ] }, + { + "cell_type": "markdown", + "id": "544f7c64-ae1e-4480-b5d0-93f0c335eccd", + "metadata": {}, + "source": [ + "In addition to that we will provide additional `dependencies` and `arguments` to our `Program` construction.\n", + "- `dependencies` parameter will install provided libraries to run our script. Dependencies can be python libraries available on PyPi or any package source installable via pip package manager .\n", + "- `arguments` parameter is a dictionary with arguments that will be passed for script execution" + ] + }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "id": "3ee09b31-4c7f-4ff3-af8f-294e4256793e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 3, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "from quantum_serverless import Program\n", + "\n", "program = Program(\n", - " name=\"Getting started program level 2\",\n", + " title=\"Getting started program level 2\",\n", " entrypoint=\"gs_level_2.py\",\n", " working_dir=\"./source_files\",\n", " dependencies=[\"qiskit-nature\", \"qiskit-nature[pyscf]\"],\n", @@ -184,11 +191,11 @@ "Running for bond length 2.55.\n", "=== GROUND STATE ENERGY ===\n", " \n", - "* Electronic ground state energy (Hartree): -8.211426457622\n", - " - computed part: -8.211426457622\n", + "* Electronic ground state energy (Hartree): -8.211426461751\n", + " - computed part: -8.211426461751\n", " - ActiveSpaceTransformer extracted energy part: 0.0\n", "~ Nuclear repulsion energy (Hartree): 0.622561424612\n", - "> Total ground state energy (Hartree): -7.58886503301\n", + "> Total ground state energy (Hartree): -7.588865037139\n", " \n", "=== MEASURED OBSERVABLES ===\n", " \n", @@ -199,11 +206,11 @@ "~ Nuclear dipole moment (a.u.): [0.0 0.0 4.81880162]\n", " \n", " 0: \n", - " * Electronic dipole moment (a.u.): [0.0 0.0 1.53218971]\n", - " - computed part: [0.0 0.0 1.53218971]\n", + " * Electronic dipole moment (a.u.): [0.0 0.0 1.53218981]\n", + " - computed part: [0.0 0.0 1.53218981]\n", " - ActiveSpaceTransformer extracted energy part: [0.0 0.0 0.0]\n", - " > Dipole moment (a.u.): [0.0 0.0 3.28661191] Total: 3.28661191\n", - " (debye): [0.0 0.0 8.35373344] Total: 8.35373344\n", + " > Dipole moment (a.u.): [0.0 0.0 3.28661181] Total: 3.28661181\n", + " (debye): [0.0 0.0 8.35373319] Total: 8.35373319\n", " \n", "\n" ] @@ -212,6 +219,152 @@ "source": [ "print(job.logs())" ] + }, + { + "cell_type": "markdown", + "id": "94e3f04f-09df-4bc0-9715-643523207516", + "metadata": {}, + "source": [ + "---\n", + "If you want to run this program with different bond length you can run it 3 times." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5f4d4317-bcc9-4e1a-942a-a38ca5331261", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[, , ]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "jobs = []\n", + "\n", + "for bond_length in [2.55, 3.0, 3.55]:\n", + " program = Program(\n", + " title=f\"Groundstate with bond length {bond_length}\",\n", + " entrypoint=\"gs_level_2.py\",\n", + " working_dir=\"./source_files\",\n", + " dependencies=[\"qiskit-nature\", \"qiskit-nature[pyscf]\"],\n", + " arguments={\n", + " \"bond_length\": bond_length\n", + " }\n", + " )\n", + " jobs.append(serverless.run_program(program))\n", + "\n", + "jobs" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0988d6b4-03a4-4c87-ad1f-5b0526a7527e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running for bond length 2.55.\n", + "=== GROUND STATE ENERGY ===\n", + " \n", + "* Electronic ground state energy (Hartree): -8.211426461751\n", + " - computed part: -8.211426461751\n", + " - ActiveSpaceTransformer extracted energy part: 0.0\n", + "~ Nuclear repulsion energy (Hartree): 0.622561424612\n", + "> Total ground state energy (Hartree): -7.588865037139\n", + " \n", + "=== MEASURED OBSERVABLES ===\n", + " \n", + " 0: # Particles: 3.997 S: 0.436 S^2: 0.626 M: 0.001\n", + " \n", + "=== DIPOLE MOMENTS ===\n", + " \n", + "~ Nuclear dipole moment (a.u.): [0.0 0.0 4.81880162]\n", + " \n", + " 0: \n", + " * Electronic dipole moment (a.u.): [0.0 0.0 1.53218981]\n", + " - computed part: [0.0 0.0 1.53218981]\n", + " - ActiveSpaceTransformer extracted energy part: [0.0 0.0 0.0]\n", + " > Dipole moment (a.u.): [0.0 0.0 3.28661181] Total: 3.28661181\n", + " (debye): [0.0 0.0 8.35373319] Total: 8.35373319\n", + " \n", + "\n", + "Running for bond length 3.0.\n", + "=== GROUND STATE ENERGY ===\n", + " \n", + "* Electronic ground state energy (Hartree): -8.124024370249\n", + " - computed part: -8.124024370249\n", + " - ActiveSpaceTransformer extracted energy part: 0.0\n", + "~ Nuclear repulsion energy (Hartree): 0.52917721092\n", + "> Total ground state energy (Hartree): -7.594847159329\n", + " \n", + "=== MEASURED OBSERVABLES ===\n", + " \n", + " 0: # Particles: 3.998 S: 0.408 S^2: 0.575 M: 0.001\n", + " \n", + "=== DIPOLE MOMENTS ===\n", + " \n", + "~ Nuclear dipole moment (a.u.): [0.0 0.0 5.66917837]\n", + " \n", + " 0: \n", + " * Electronic dipole moment (a.u.): [0.0 0.0 2.91821215]\n", + " - computed part: [0.0 0.0 2.91821215]\n", + " - ActiveSpaceTransformer extracted energy part: [0.0 0.0 0.0]\n", + " > Dipole moment (a.u.): [0.0 0.0 2.75096622] Total: 2.75096622\n", + " (debye): [0.0 0.0 6.99225801] Total: 6.99225801\n", + " \n", + "\n", + "Running for bond length 3.55.\n", + "=== GROUND STATE ENERGY ===\n", + " \n", + "* Electronic ground state energy (Hartree): -8.049823374615\n", + " - computed part: -8.049823374615\n", + " - ActiveSpaceTransformer extracted energy part: 0.0\n", + "~ Nuclear repulsion energy (Hartree): 0.447192009228\n", + "> Total ground state energy (Hartree): -7.602631365386\n", + " \n", + "=== MEASURED OBSERVABLES ===\n", + " \n", + " 0: # Particles: 3.999 S: 0.378 S^2: 0.520 M: 0.001\n", + " \n", + "=== DIPOLE MOMENTS ===\n", + " \n", + "~ Nuclear dipole moment (a.u.): [0.0 0.0 6.70852774]\n", + " \n", + " 0: \n", + " * Electronic dipole moment (a.u.): [0.0 0.0 5.39179177]\n", + " - computed part: [0.0 0.0 5.39179177]\n", + " - ActiveSpaceTransformer extracted energy part: [0.0 0.0 0.0]\n", + " > Dipole moment (a.u.): [0.0 0.0 1.31673597] Total: 1.31673597\n", + " (debye): [0.0 0.0 3.34680868] Total: 3.34680868\n", + " \n", + "\n" + ] + } + ], + "source": [ + "for job in jobs:\n", + " print(job.logs())" + ] + }, + { + "cell_type": "markdown", + "id": "95a1d859-a089-4d7f-ad88-b2acae1ed66d", + "metadata": {}, + "source": [ + "---\n", + "Other way would be refactoring program file itself to accept list of bond length and run them in a loop inside a program" + ] } ], "metadata": { diff --git a/docs/getting_started/01_intro_level_3.ipynb b/docs/getting_started/01_intro_level_3.ipynb index f664ffb9e..d8fe6b33f 100644 --- a/docs/getting_started/01_intro_level_3.ipynb +++ b/docs/getting_started/01_intro_level_3.ipynb @@ -9,8 +9,8 @@ "\n", "In this tutorial we will explore a little bit more advanced example of a program that require some configuration, requirements setup, etc. \n", "\n", - "Again we will start with writing code for our program and saving it to `./source_files/gs_level_3.py` file.\n", - "This time it will be running estimator as parallel functions and saving results to shared state. \n", + "Again we will start with writing code for our program and saving it to [./source_files/gs_level_3.py](./source_files/gs_level_3.py) file.\n", + "This time, our program will run an estimator as a parallel function, computing the expectation value of a single observable over a set of random circuits. The results will be saved to a database, which means it will be stored in a formatted way and later on we can fetch results of or programs without looking at logs.\n", "\n", "```python\n", "# source_files/gs_level_3.py\n", @@ -20,134 +20,102 @@ "from qiskit.quantum_info import SparsePauliOp\n", "from qiskit.primitives import Estimator\n", "\n", - "from quantum_serverless import QuantumServerless, run_qiskit_remote, get, put\n", - "from quantum_serverless.core.state import RedisStateHandler\n", + "from quantum_serverless import QuantumServerless, run_qiskit_remote, get, put, save_result\n", "\n", "# 1. let's annotate out function to convert it\n", "# to function that can be executed remotely\n", "# using `run_qiskit_remote` decorator\n", "@run_qiskit_remote()\n", "def my_function(circuit: QuantumCircuit, obs: SparsePauliOp):\n", + " \"\"\"Compute expectation value of an obs given a circuit\"\"\"\n", " return Estimator().run([circuit], [obs]).result().values\n", "\n", "\n", - "# 2. Next let's create out serverless object to control\n", - "# where our remote function will be executed\n", + "# 2. Next let's create our serverless object that we will be using to create context\n", + "# which will allow us to run funcitons in parallel\n", "serverless = QuantumServerless()\n", "\n", - "# 2.1 (Optional) state handler to write/read results in/out of job\n", - "state_handler = RedisStateHandler(\"redis\", 6379)\n", - "\n", "circuits = [random_circuit(2, 2) for _ in range(3)]\n", "\n", - "# 3. create serverless context\n", + "# 3. create serverless context which will allow us to run functions in parallel\n", "with serverless.context():\n", - " # 4. let's put some shared objects into remote storage that will be shared among all executions\n", + " # 4. The observable is the same for all expectation value calculations. So we can put that object into remote storage since it will be shared among all executions of my_function.\n", " obs_ref = put(SparsePauliOp([\"ZZ\"]))\n", "\n", - " # 4. run our function and get back reference to it\n", - " # as now our function it remote one\n", + " # 5. we can run our function for a single input circuit \n", + " # and get back a reference to it as now our function is a remote one\n", " function_reference = my_function(circuits[0], obs_ref)\n", "\n", - " # 4.1 or we can run N of them in parallel (for all circuits)\n", + " # 5.1 or we can run N of them in parallel (for all circuits)\n", + " # note: if we will be using real backends (QPUs) we should either use\n", + " # N separate backends to run them in parallel or\n", + " # one will be running after each other sequentially\n", " function_references = [my_function(circ, obs_ref) for circ in circuits]\n", "\n", - " # 5. to get results back from reference\n", + " # 6. to get results back from reference\n", " # we need to call `get` on function reference\n", " single_result = get(function_reference)\n", " parallel_result = get(function_references)\n", " print(\"Single execution:\", single_result)\n", " print(\"N parallel executions:\", parallel_result)\n", "\n", - " # 5.1 (Optional) write results to state.\n", - " state_handler.set(\"result\", {\n", + " # 6.1 (Optional) write results to db.\n", + " save_result({\n", " \"status\": \"ok\",\n", " \"single\": single_result.tolist(),\n", " \"parallel_result\": [entry.tolist() for entry in parallel_result]\n", " })\n", + "\n", "```\n", "\n", "As you can see we move to advanced section of using serverless. \n", "\n", - "Here we are using `run_qiskit_remote` decorator to convert our function to parallel one. \n", - "With that `my_function` is converted into remote call (as a result you will be getting function pointer) and in order to fetch results of this function we need to call `get` function.\n", + "Here we are using `run_qiskit_remote` decorator to convert our function to asyncronous distributed one. \n", + "With that `my_function` is converted into asyncronous distributed function (as a result you will be getting function pointer), which means that the function no longer executes as part of your local python process, but executed on configured compute resources.\n", "\n", - "Moreover, we are using `RedisStateHandler` in order to save results into state storage, so we can retrieve it later after program execution.\n", + "Moreover, we are using `save_result` function in order to save results into database storage, so we can retrieve it later after program execution.\n", "\n", "Next we need to run this program. For that we need to import necessary modules and configure QuantumServerless client. We are doing so by providing name and host for deployed infrastructure." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "id": "9130c64a-1e7f-4d08-afff-b2905b2d95ad", "metadata": {}, "outputs": [], "source": [ - "from quantum_serverless import QuantumServerless, Program\n", - "from quantum_serverless.core.state import RedisStateHandler" + "from quantum_serverless import QuantumServerless, GatewayProvider" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "id": "0f22daae-9f0e-4f7a-8a1f-5ade989d8be9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 2, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "serverless = QuantumServerless({\n", - " \"providers\": [{\n", - " \"name\": \"docker\",\n", - " \"compute_resource\": {\n", - " \"name\": \"docker\",\n", - " \"host\": \"localhost\",\n", - " }\n", - " }]\n", - "})\n", + "provider = GatewayProvider(\n", + " username=\"john\", # username is predefined in local docker setup\n", + " password=\"password123\", # password is predefined in local docker setup\n", + " host=\"http://localhost:8000\", # address of provider\n", + ")\n", + "\n", + "serverless = QuantumServerless(provider)\n", "serverless" ] }, - { - "cell_type": "markdown", - "id": "1f2e4c59-6a45-4e8e-919e-89e66c9727a1", - "metadata": {}, - "source": [ - "We will create instance of state handler to fetch results from program after execution" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "28a77e08-e542-4259-af67-1c70b8498f68", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "state_handler = RedisStateHandler(\"localhost\", 6379)\n", - "state_handler" - ] - }, { "cell_type": "markdown", "id": "3321b4a0-b60d-433a-992a-79e5868d309b", @@ -158,24 +126,26 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "id": "f556dd85-35da-48d1-9ae1-f04a386544d9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 4, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "from quantum_serverless import Program \n", + "\n", "program = Program(\n", - " name=\"Advanced program\",\n", + " title=\"Advanced program\",\n", " entrypoint=\"gs_level_3.py\",\n", " working_dir=\"./source_files/\"\n", ")\n", @@ -186,17 +156,17 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "2de3fd64-9010-48d9-ac7c-f46a7b36ba81", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "'SUCCEEDED'" ] }, - "execution_count": 9, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -207,7 +177,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "id": "d6586e7a-388b-42cc-a860-abd4f6d514b9", "metadata": {}, "outputs": [ @@ -216,7 +186,7 @@ "output_type": "stream", "text": [ "Single execution: [1.]\n", - "N parallel executions: [array([1.]), array([0.]), array([-1.])]\n", + "N parallel executions: [array([1.]), array([0.97400357]), array([1.])]\n", "\n" ] } @@ -230,28 +200,28 @@ "id": "29336f0b-ffcf-4cdb-931c-11faf09f15ff", "metadata": {}, "source": [ - "With `state_handler` as can fetch results that we wrote inside the program." + "With `job.result()` we can get saved results inside of our function back." ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "id": "1fb8931f-c8e2-49dd-923f-16fa3a7a5feb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'status': 'ok', 'single': [1.0], 'parallel_result': [[1.0], [0.0], [-1.0]]}" + "'{\"status\": \"ok\", \"single\": [1.0], \"parallel_result\": [[1.0], [0.9740035726118753], [1.0]]}'" ] }, - "execution_count": 11, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "state_handler.get(\"result\")" + "job.result()" ] } ], diff --git a/docs/getting_started/source_files/gs_level_3.py b/docs/getting_started/source_files/gs_level_3.py index 0dcec4965..8d0548a9a 100644 --- a/docs/getting_started/source_files/gs_level_3.py +++ b/docs/getting_started/source_files/gs_level_3.py @@ -3,47 +3,45 @@ from qiskit.quantum_info import SparsePauliOp from qiskit.primitives import Estimator -from quantum_serverless import QuantumServerless, run_qiskit_remote, get, put -from quantum_serverless.core.state import RedisStateHandler +from quantum_serverless import QuantumServerless, run_qiskit_remote, get, put, save_result # 1. let's annotate out function to convert it # to function that can be executed remotely # using `run_qiskit_remote` decorator @run_qiskit_remote() def my_function(circuit: QuantumCircuit, obs: SparsePauliOp): + """Compute expectation value of an obs given a circuit""" return Estimator().run([circuit], [obs]).result().values -# 2. Next let's create out serverless object to control -# where our remote function will be executed -serverless = QuantumServerless() -# 2.1 (Optional) state handler to write/read results in/out of job -state_handler = RedisStateHandler("redis", 6379) +# 2. Next let's create our serverless object that we will be using to create context +# which will allow us to run funcitons in parallel +serverless = QuantumServerless() circuits = [random_circuit(2, 2) for _ in range(3)] -# 3. create serverless context +# 3. create serverless context which will allow us to run funcitons in parallel with serverless.context(): - # 4. let's put some shared objects into remote storage that will be shared among all executions + # 4. The observable is the same for all expectation value calculations. So we can put that object into remote storage since it will be shared among all executions of my_function. obs_ref = put(SparsePauliOp(["ZZ"])) - # 4. run our function and get back reference to it - # as now our function it remote one + # 5. we can run our function for a single input circuit + # and get back a reference to it as now our function is a remote one function_reference = my_function(circuits[0], obs_ref) - # 4.1 or we can run N of them in parallel (for all circuits) + # 5.1 or we can run N of them in parallel (for all circuits) function_references = [my_function(circ, obs_ref) for circ in circuits] - # 5. to get results back from reference + # 6. to get results back from reference # we need to call `get` on function reference single_result = get(function_reference) parallel_result = get(function_references) print("Single execution:", single_result) print("N parallel executions:", parallel_result) - # 5.1 (Optional) write results to state. - state_handler.set("result", { + # 6.1 (Optional) write results to db. + save_result({ "status": "ok", "single": single_result.tolist(), "parallel_result": [entry.tolist() for entry in parallel_result] From c43b66c3ea378cbf44c492600dba82ccc23c37fa Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Tue, 21 Mar 2023 17:57:11 -0400 Subject: [PATCH 12/15] Issue 217 | Docker-compose: update realm --- docker-compose.yml | 13 +- realm-export.json | 1793 +++++++++++++++++++++++--------------------- 2 files changed, 928 insertions(+), 878 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 03eb8feaa..193b67ab5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -73,7 +73,7 @@ services: KEYCLOAK_ADMIN_USER: admin KEYCLOAK_ADMIN_PASSWORD: 123 KEYCLOAK_HTTP_PORT: 8080 - KEYCLOAK_EXTRA_ARGS: "--import-realm" + KEYCLOAK_EXTRA_ARGS: "-Dkeycloak.import=/opt/keycloak/data/import/realm-export.json" ports: - '8085:8080' depends_on: @@ -85,19 +85,20 @@ services: gateway: container_name: gateway image: docker.io/qiskit/quantum-serverless-gateway:nightly - command: gunicorn gateway.wsgi:application --bind 0.0.0.0:8000 --workers=3 + command: python manage.py runserver 0.0.0.0:8000 ports: - 8000:8000 environment: - DEBUG=0 - RAY_HOST=http://ray-head:8265 - - CLIENT_ID=gateway-client + - CLIENT_ID=rayclient - DJANGO_SUPERUSER_USERNAME=admin - DJANGO_SUPERUSER_PASSWORD=123 - DJANGO_SUPERUSER_EMAIL=admin@noemail.com - - SETTING_KEYCLOAK_URL=http://keycloak:8080/auth - - SETTING_KEYCLOAK_REALM=Test - - SETTINGS_KEYCLOAK_CLIENT_SECRET=AQ3sZ4eiF7NhOtfxeUEGo0YN7uQBoUnO + - SETTING_KEYCLOAK_URL=http://keycloak:8080/ + - SETTING_KEYCLOAK_REALM=quantumserverless + - SETTINGS_KEYCLOAK_CLIENT_SECRET=supersecret + - SETTINGS_KEYCLOAK_CLIENT_NAME=rayclient - SITE_HOST=http://gateway:8000 networks: - safe-tier diff --git a/realm-export.json b/realm-export.json index 8b48511a6..1ce97b459 100644 --- a/realm-export.json +++ b/realm-export.json @@ -1,6 +1,6 @@ { - "id": "Test", - "realm": "Test", + "id": "e165160a-1516-4a29-b65c-6f4479682dc3", + "realm": "quantumserverless", "notBefore": 0, "defaultSignatureAlgorithm": "RS256", "revokeRefreshToken": false, @@ -26,15 +26,15 @@ "oauth2DeviceCodeLifespan": 600, "oauth2DevicePollingInterval": 5, "enabled": true, - "sslRequired": "none", - "registrationAllowed": true, - "registrationEmailAsUsername": true, - "rememberMe": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, "verifyEmail": false, "loginWithEmailAllowed": true, "duplicateEmailsAllowed": false, - "resetPasswordAllowed": true, - "editUsernameAllowed": true, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, "bruteForceProtected": false, "permanentLockout": false, "maxFailureWaitSeconds": 900, @@ -46,295 +46,372 @@ "roles": { "realm": [ { - "id": "4833bad1-0ba1-4115-a2e1-3b96a90fe268", - "name": "default-roles-test", + "id": "aafad90c-2bb2-4fe9-9403-278abb4e3e95", + "name": "default-roles-quantumserverless", "description": "${role_default-roles}", "composite": true, "composites": { "realm": [ "offline_access", "uma_authorization" - ] + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } }, "clientRole": false, - "containerId": "Test", + "containerId": "e165160a-1516-4a29-b65c-6f4479682dc3", "attributes": {} }, { - "id": "556c8a27-7a33-4ea5-9232-525239ff6807", - "name": "offline_access", - "description": "${role_offline-access}", + "id": "84eed94c-1c9a-45c9-b826-31066ac74042", + "name": "uma_authorization", + "description": "${role_uma_authorization}", "composite": false, "clientRole": false, - "containerId": "Test", + "containerId": "e165160a-1516-4a29-b65c-6f4479682dc3", "attributes": {} }, { - "id": "0ad4b9c1-aad6-4b13-ae28-7ff0cb447dc0", - "name": "uma_authorization", - "description": "${role_uma_authorization}", + "id": "33e9530c-9233-4108-a3c1-c4dcd205fe60", + "name": "offline_access", + "description": "${role_offline-access}", "composite": false, "clientRole": false, - "containerId": "Test", + "containerId": "e165160a-1516-4a29-b65c-6f4479682dc3", "attributes": {} } ], "client": { "realm-management": [ { - "id": "5af1d2f0-30bb-4483-bab5-210e061e2f1d", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", + "id": "4354f1b3-b8d1-45c0-996f-2b37282b2039", + "name": "query-clients", + "description": "${role_query-clients}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "68d8756c-1b28-4dd0-9bde-1255b8f753c4", - "name": "view-realm", - "description": "${role_view-realm}", + "id": "16595b14-fa8e-45ac-a4c5-8cbe51c82b9c", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "7b54160f-01d4-43d8-8bd3-101bd491a28e", - "name": "create-client", - "description": "${role_create-client}", + "id": "727f3927-0772-4427-8955-af4467bf340c", + "name": "impersonation", + "description": "${role_impersonation}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "38bcccc5-64f6-475a-a875-8fe6d8d12f95", - "name": "view-users", - "description": "${role_view-users}", + "id": "9b8fe7cc-8568-4a47-a86e-93a2c09749a5", + "name": "realm-admin", + "description": "${role_realm-admin}", "composite": true, "composites": { "client": { "realm-management": [ + "query-clients", + "view-identity-providers", + "impersonation", + "manage-users", + "manage-identity-providers", "query-users", - "query-groups" + "view-clients", + "view-events", + "manage-authorization", + "manage-events", + "query-groups", + "view-realm", + "manage-clients", + "view-authorization", + "manage-realm", + "query-realms", + "create-client", + "view-users" ] } }, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "702884ce-65f4-4899-97b3-1ff3b585039f", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", + "id": "ed331d75-7f38-42af-8bc1-a9d724b90b61", + "name": "manage-users", + "description": "${role_manage-users}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "07f8bd0b-e340-42b3-91c8-d1a2b5bd88c5", - "name": "manage-realm", - "description": "${role_manage-realm}", + "id": "ec1b4639-c703-4243-81b2-95e56f4a4e12", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "8c9d0de4-a7dc-43e9-a601-e3e57cb114df", - "name": "realm-admin", - "description": "${role_realm-admin}", + "id": "35859f1f-3bb8-459b-b387-4f66ac8ab6ac", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", + "attributes": {} + }, + { + "id": "f7fad160-0d57-484e-bebf-3fdc25ad248f", + "name": "view-clients", + "description": "${role_view-clients}", "composite": true, "composites": { "client": { "realm-management": [ - "view-realm", - "manage-identity-providers", - "create-client", - "view-users", - "view-identity-providers", - "manage-realm", - "manage-authorization", - "query-realms", - "manage-users", - "query-groups", - "impersonation", - "manage-clients", - "manage-events", - "query-users", - "view-authorization", - "view-clients", - "view-events", "query-clients" ] } }, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "21f3ed07-221a-4d2c-a3ca-8df78cca9f76", - "name": "manage-authorization", - "description": "${role_manage-authorization}", + "id": "bba8481b-4c74-4136-bc9a-00aee0275302", + "name": "view-events", + "description": "${role_view-events}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "69e415af-8434-4c00-838f-a608a6123e20", - "name": "query-realms", - "description": "${role_query-realms}", + "id": "02bd61cb-0bc2-4a1f-8b5e-0b353fa9a3d7", + "name": "manage-authorization", + "description": "${role_manage-authorization}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "08a45a5b-d94a-486f-a021-75b7c85ecb67", - "name": "impersonation", - "description": "${role_impersonation}", + "id": "d4a59f44-a1a7-4be2-889c-77766d4cff12", + "name": "manage-events", + "description": "${role_manage-events}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "a620f375-f013-4474-adb8-a8749d0fe039", - "name": "manage-users", - "description": "${role_manage-users}", + "id": "01a5a7d4-6c57-4312-ba6f-1abf21848343", + "name": "query-groups", + "description": "${role_query-groups}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "02aaa954-c09e-41a9-9933-aed44514d94e", - "name": "query-groups", - "description": "${role_query-groups}", + "id": "43ccc28e-3a4b-44b5-8285-8f10755dee46", + "name": "view-realm", + "description": "${role_view-realm}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "a1f26bd1-6703-4b88-9d17-eed35ba03fd5", + "id": "7107fc0d-ec23-4ff9-89cd-0294be30b437", "name": "manage-clients", "description": "${role_manage-clients}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "eebbe4e0-0079-4f9a-91ca-27994aa35d91", - "name": "manage-events", - "description": "${role_manage-events}", + "id": "8a478cd7-9139-4b4c-ae23-1e04ef4add89", + "name": "view-authorization", + "description": "${role_view-authorization}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "d58170d1-fee7-4eac-b2e0-5dd3a95a3433", - "name": "query-users", - "description": "${role_query-users}", + "id": "70dd0b05-3ee8-47ed-9023-17d50419e6cf", + "name": "manage-realm", + "description": "${role_manage-realm}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "1ef1a2b4-ea4f-4951-8452-60fb9148e0ed", - "name": "view-authorization", - "description": "${role_view-authorization}", + "id": "ee14d66f-004d-4b4c-a5d4-fdd817983d17", + "name": "query-realms", + "description": "${role_query-realms}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} }, { - "id": "e122d715-8b55-4997-b79b-ad81899f854a", - "name": "view-clients", - "description": "${role_view-clients}", + "id": "14b30847-c0b3-4cb6-bfd5-fbd44be055f3", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", + "attributes": {} + }, + { + "id": "59b8e66a-4f6e-46a4-a139-5b6059dfd62b", + "name": "view-users", + "description": "${role_view-users}", "composite": true, "composites": { "client": { "realm-management": [ - "query-clients" + "query-groups", + "query-users" ] } }, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", "attributes": {} - }, + } + ], + "rayclient": [], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ { - "id": "cf138fd9-4093-4715-a4e0-edaf6bbd1e56", - "name": "view-events", - "description": "${role_view-events}", + "id": "c6de7240-0694-42d7-be14-56d3964c9e07", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "85eca027-a1f5-44d9-849e-2a37725808ad", + "attributes": {} + } + ], + "account": [ + { + "id": "efb98987-17f5-485a-80f8-d74a5bd7edb5", + "name": "manage-account-links", + "description": "${role_manage-account-links}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "8cb563b9-d51a-4d1a-891d-39f40e0b5d6c", "attributes": {} }, { - "id": "97d75e8b-ef13-4084-adc4-fa70df05d52f", - "name": "query-clients", - "description": "${role_query-clients}", + "id": "dbe777af-f7c7-4030-b1aa-df83e7d9de0e", + "name": "view-groups", + "description": "${role_view-groups}", "composite": false, "clientRole": true, - "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "containerId": "8cb563b9-d51a-4d1a-891d-39f40e0b5d6c", "attributes": {} - } - ], - "security-admin-console": [], - "admin-cli": [], - "gateway-client": [ + }, { - "id": "3f58b737-9ffa-455e-a36b-5a1b3f089080", - "name": "uma_protection", + "id": "17760ec2-5efa-423d-8c0a-53ed92366bfd", + "name": "view-applications", + "description": "${role_view-applications}", "composite": false, "clientRole": true, - "containerId": "9a76b2ec-b33e-40b0-9cad-e00ca7e77e40", + "containerId": "8cb563b9-d51a-4d1a-891d-39f40e0b5d6c", "attributes": {} - } - ], - "account-console": [], - "broker": [], - "account": [ + }, { - "id": "5d39ee7c-40a9-4656-a6f7-05efc0b00002", + "id": "b250e972-4d1c-41fc-a68b-6e273673f8d8", "name": "delete-account", "description": "${role_delete-account}", "composite": false, "clientRole": true, - "containerId": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "containerId": "8cb563b9-d51a-4d1a-891d-39f40e0b5d6c", + "attributes": {} + }, + { + "id": "be4828b4-483b-4975-96dd-f2c55f9f30d7", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "8cb563b9-d51a-4d1a-891d-39f40e0b5d6c", + "attributes": {} + }, + { + "id": "a4df2eb7-206a-45fa-af6a-174826af5e4e", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "8cb563b9-d51a-4d1a-891d-39f40e0b5d6c", "attributes": {} }, { - "id": "e6b688f3-4c5b-4381-96ff-9f6617a9c515", + "id": "f73af6e8-5aed-48d5-95b2-b69383c26f44", "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "8cb563b9-d51a-4d1a-891d-39f40e0b5d6c", + "attributes": {} + }, + { + "id": "9755cf13-2aa7-4e97-9b01-07370d0d4033", + "name": "view-profile", + "description": "${role_view-profile}", "composite": false, "clientRole": true, - "containerId": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "containerId": "8cb563b9-d51a-4d1a-891d-39f40e0b5d6c", "attributes": {} } - ] + ], + "rayapiserver": [] } }, "groups": [], "defaultRole": { - "id": "4833bad1-0ba1-4115-a2e1-3b96a90fe268", - "name": "default-roles-test", + "id": "aafad90c-2bb2-4fe9-9403-278abb4e3e95", + "name": "default-roles-quantumserverless", "description": "${role_default-roles}", "composite": true, "clientRole": false, - "containerId": "Test" + "containerId": "e165160a-1516-4a29-b65c-6f4479682dc3" }, "requiredCredentials": [ "password" @@ -345,9 +422,10 @@ "otpPolicyDigits": 6, "otpPolicyLookAheadWindow": 1, "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, "otpSupportedApplications": [ - "FreeOTP", - "Google Authenticator" + "totpAppFreeOTPName", + "totpAppGoogleName" ], "webAuthnPolicyRpEntityName": "keycloak", "webAuthnPolicySignatureAlgorithms": [ @@ -374,78 +452,41 @@ "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, "webAuthnPolicyPasswordlessAcceptableAaguids": [], "users": [ + { + "id": "117269c7-3630-4810-a537-1be0f0749371", + "createdTimestamp": 1676908028992, + "username": "service-account-rayapiserver", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "rayapiserver", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-quantumserverless" + ], + "notBefore": 0, + "groups": [] + }, { "username": "john", "enabled": true, + "email": "user@quatunserverless.org", "emailVerified": true, - "email": "john@example.com", - "firstName": "John", - "lastName": "Doe", "credentials": [ { "type": "password", - "value": "password123", - "temporary": false + "value": "password123" } - ] - }, - { - "id": "33b940e2-0bdb-49a7-9356-e6e230f49619", - "createdTimestamp": 1640089861472, - "username": "service-account-admin-cli", - "enabled": true, - "totp": false, - "emailVerified": false, - "serviceAccountClientId": "admin-cli", - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": [ - "offline_access", - "default-roles-test", - "uma_authorization" ], "clientRoles": { "realm-management": [ - "manage-identity-providers", - "view-realm", - "create-client", - "view-users", - "view-identity-providers", - "manage-realm", - "realm-admin", - "manage-authorization", - "query-realms", - "impersonation", - "manage-users", - "query-groups", - "manage-clients", - "manage-events", - "query-users", - "view-authorization", - "view-clients", - "view-events", - "query-clients" + "realm-admin" ], "account": [ - "delete-account", "manage-account" ] - }, - "notBefore": 0, - "groups": [] - }, - { - "id": "83d84b8e-f053-480e-8b13-713c4fac708d", - "createdTimestamp": 1640089810342, - "username": "service-account-gateway-client", - "enabled": true, - "totp": false, - "emailVerified": false, - "serviceAccountClientId": "gateway-client", - "disableableCredentialTypes": [], - "requiredActions": [], - "notBefore": 0, - "groups": [] + } } ], "scopeMappings": [ @@ -457,36 +498,29 @@ } ], "clientScopeMappings": { - "gateway-client": [ - { - "client": "admin-cli", - "roles": [ - "uma_protection" - ] - } - ], "account": [ { "client": "account-console", "roles": [ - "manage-account" + "manage-account", + "view-groups" ] } ] }, "clients": [ { - "id": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "id": "8cb563b9-d51a-4d1a-891d-39f40e0b5d6c", "clientId": "account", "name": "${client_account}", "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/Test/account/", + "baseUrl": "/realms/quantumserverless/account/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ - "/realms/Test/account/*" + "/realms/quantumserverless/account/*" ], "webOrigins": [], "notBefore": 0, @@ -499,12 +533,15 @@ "publicClient": true, "frontchannelLogout": false, "protocol": "openid-connect", - "attributes": {}, + "attributes": { + "post.logout.redirect.uris": "+" + }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, "defaultClientScopes": [ "web-origins", + "acr", "roles", "profile", "email" @@ -517,17 +554,17 @@ ] }, { - "id": "207a4d3c-cc80-4bd2-91d4-815a1af38778", + "id": "24b16ed9-06d9-4340-898d-6cb1ac946657", "clientId": "account-console", "name": "${client_account-console}", "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/Test/account/", + "baseUrl": "/realms/quantumserverless/account/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ - "/realms/Test/account/*" + "/realms/quantumserverless/account/*" ], "webOrigins": [], "notBefore": 0, @@ -541,6 +578,7 @@ "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { + "post.logout.redirect.uris": "+", "pkce.code.challenge.method": "S256" }, "authenticationFlowBindingOverrides": {}, @@ -548,7 +586,7 @@ "nodeReRegistrationTimeout": 0, "protocolMappers": [ { - "id": "70d4fa1a-79b2-489e-b9a0-47a6772819a6", + "id": "12979a04-8cbb-49ce-8561-a85c6d0e5c5e", "name": "audience resolve", "protocol": "openid-connect", "protocolMapper": "oidc-audience-resolve-mapper", @@ -558,6 +596,7 @@ ], "defaultClientScopes": [ "web-origins", + "acr", "roles", "profile", "email" @@ -570,14 +609,13 @@ ] }, { - "id": "f8f4baad-a231-4a6a-b97c-5d68ac147279", + "id": "196abcdd-7de5-4864-8d81-75ca5ac23fa9", "clientId": "admin-cli", "name": "${client_admin-cli}", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "NKlUMjdSJBcnMkJBPhQwXQQfbtJfAyme", "redirectUris": [], "webOrigins": [], "notBefore": 0, @@ -586,87 +624,19 @@ "standardFlowEnabled": false, "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": true, - "publicClient": false, + "serviceAccountsEnabled": false, + "publicClient": true, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "saml.encrypt": "false", - "oauth2.device.authorization.grant.enabled": "true", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "exclude.session.state.from.auth.response": "false", - "oidc.ciba.grant.enabled": "false", - "saml.artifact.binding": "false", - "backchannel.logout.session.required": "false", - "client_credentials.use_refresh_token": "false", - "saml_force_name_id_format": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "saml.onetimeuse.condition": "false" + "post.logout.redirect.uris": "+" }, "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, + "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "a73b0f3e-1b0c-4b14-893e-22f4985cfd60", - "name": "Client Host", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientHost", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientHost", - "jsonType.label": "String" - } - }, - { - "id": "030a393a-ff89-4d2e-aa30-063e95b7ce9f", - "name": "Client ID", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientId", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientId", - "jsonType.label": "String" - } - }, - { - "id": "8e4e8915-cba7-4be3-86e8-d6991a0cd273", - "name": "Client IP Address", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientAddress", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientAddress", - "jsonType.label": "String" - } - } - ], "defaultClientScopes": [ "web-origins", + "acr", "roles", "profile", "email" @@ -676,53 +646,10 @@ "phone", "offline_access", "microprofile-jwt" - ], - "authorizationSettings": { - "allowRemoteResourceManagement": true, - "policyEnforcementMode": "ENFORCING", - "resources": [ - { - "name": "Default Resource", - "type": "urn:admin-cli:resources:default", - "ownerManagedAccess": false, - "attributes": {}, - "_id": "98ea544d-9474-4cde-a7d5-f4aa8438596b", - "uris": [ - "/*" - ] - } - ], - "policies": [ - { - "id": "3747a4f9-0b6b-4ad0-aba4-181193729727", - "name": "Default Policy", - "description": "A policy that grants access only for users within this realm", - "type": "js", - "logic": "POSITIVE", - "decisionStrategy": "AFFIRMATIVE", - "config": { - "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" - } - }, - { - "id": "762a6303-aab7-439b-8a41-0973964640ce", - "name": "Default Permission", - "description": "A permission that applies to the default resource type", - "type": "resource", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "defaultResourceType": "urn:admin-cli:resources:default", - "applyPolicies": "[\"Default Policy\"]" - } - } - ], - "scopes": [], - "decisionStrategy": "UNANIMOUS" - } + ] }, { - "id": "1d1a4841-fbfe-4bda-9bc8-fdc73497aa5c", + "id": "85eca027-a1f5-44d9-849e-2a37725808ad", "clientId": "broker", "name": "${client_broker}", "surrogateAuthRequired": false, @@ -741,12 +668,15 @@ "publicClient": false, "frontchannelLogout": false, "protocol": "openid-connect", - "attributes": {}, + "attributes": { + "post.logout.redirect.uris": "+" + }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, "defaultClientScopes": [ "web-origins", + "acr", "roles", "profile", "email" @@ -759,34 +689,92 @@ ] }, { - "id": "fb6c4935-1d0c-4e82-b262-443672d72930", - "clientId": "realm-management", - "name": "${client_realm-management}", + "id": "4f64e9b4-034d-48fd-b00b-0582e1017b71", + "clientId": "rayapiserver", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", + "secret": "supersecret", "redirectUris": [], "webOrigins": [], "notBefore": 0, - "bearerOnly": true, + "bearerOnly": false, "consentRequired": false, - "standardFlowEnabled": true, + "standardFlowEnabled": false, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, + "serviceAccountsEnabled": true, "publicClient": false, - "frontchannelLogout": false, + "frontchannelLogout": true, "protocol": "openid-connect", - "attributes": {}, + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1676907939", + "backchannel.logout.session.required": "true", + "display.on.consent.screen": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "d9421bc1-c83e-4c5d-8f23-0338e2dc0a22", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "19625f49-50fd-467d-81ca-24c84139133f", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "48c6d133-d94e-4450-8bfe-16ab8c8ae2ad", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + } + ], "defaultClientScopes": [ "web-origins", + "acr", "roles", "profile", - "email" + "email", + "rayapiserver" ], "optionalClientScopes": [ "address", @@ -796,56 +784,47 @@ ] }, { - "id": "97d658fa-02d4-43d5-9bba-4d0717a8466d", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/Test/console/", + "id": "5d01b1dd-fbc8-48b6-bf55-2748057607a8", + "clientId": "rayclient", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", + "secret": "supersecret", "redirectUris": [ - "/admin/Test/console/*" - ], - "webOrigins": [ - "+" + "http://localhost/oauth2/callback" ], + "webOrigins": [], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, + "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, + "publicClient": false, + "frontchannelLogout": true, "protocol": "openid-connect", "attributes": { - "pkce.code.challenge.method": "S256" + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1675977386", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "display.on.consent.screen": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" }, "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "fb2e09ee-c7b0-49b2-870d-758173ec6be7", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ], + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, "defaultClientScopes": [ "web-origins", + "acr", "roles", "profile", "email" @@ -858,13 +837,17 @@ ] }, { - "id": "9a76b2ec-b33e-40b0-9cad-e00ca7e77e40", "clientId": "gateway-client", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "AQ3sZ4eiF7NhOtfxeUEGo0YN7uQBoUnO", + "secret": "supersecret", "redirectUris": [ "*" ], @@ -875,87 +858,134 @@ "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": true, + "serviceAccountsEnabled": false, "publicClient": false, - "frontchannelLogout": false, + "frontchannelLogout": true, "protocol": "openid-connect", "attributes": { - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "saml.encrypt": "false", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "exclude.session.state.from.auth.response": "false", "oidc.ciba.grant.enabled": "false", - "saml.artifact.binding": "false", + "client.secret.creation.time": "1679332636", "backchannel.logout.session.required": "true", - "client_credentials.use_refresh_token": "false", - "saml_force_name_id_format": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", + "post.logout.redirect.uris": "*", "display.on.consent.screen": "false", - "saml.onetimeuse.condition": "false" + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" }, "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, + "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + } + }, + { + "id": "744a1aec-f818-4fd3-9fd5-d2f49c282e06", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a6c685db-a00a-4250-bd64-31bd5b0d3b2a", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/quantumserverless/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/quantumserverless/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, "protocolMappers": [ { - "id": "3716053c-9672-4685-9fe5-0b44307c65c1", - "name": "Client IP Address", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientAddress", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientAddress", - "jsonType.label": "String" - } - }, - { - "id": "4cffb7d8-1aab-4b35-8111-df1ee341c76a", - "name": "Client ID", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientId", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientId", - "jsonType.label": "String" - } - }, - { - "id": "57540600-0bd8-42dd-8eb1-ca4177c2da57", - "name": "Client Host", + "id": "28149aa5-13e8-4c53-9304-3f94c29bdaea", + "name": "locale", "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", + "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "user.session.note": "clientHost", "userinfo.token.claim": "true", + "user.attribute": "locale", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "clientHost", + "claim.name": "locale", "jsonType.label": "String" } } ], "defaultClientScopes": [ "web-origins", + "acr", "roles", "profile", "email" @@ -965,55 +995,12 @@ "phone", "offline_access", "microprofile-jwt" - ], - "authorizationSettings": { - "allowRemoteResourceManagement": true, - "policyEnforcementMode": "ENFORCING", - "resources": [ - { - "name": "Default Resource", - "type": "urn:gateway-client:resources:default", - "ownerManagedAccess": false, - "attributes": {}, - "_id": "c4c07a91-21b2-4259-b923-4b3d6b05d93f", - "uris": [ - "/*" - ] - } - ], - "policies": [ - { - "id": "b1174446-ce63-4d3d-8829-f1b960a76b42", - "name": "Default Policy", - "description": "A policy that grants access only for users within this realm", - "type": "js", - "logic": "POSITIVE", - "decisionStrategy": "AFFIRMATIVE", - "config": { - "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" - } - }, - { - "id": "c595a3a7-c4d3-47b1-896d-50e5396d1eee", - "name": "Default Permission", - "description": "A permission that applies to the default resource type", - "type": "resource", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "defaultResourceType": "urn:gateway-client:resources:default", - "applyPolicies": "[\"Default Policy\"]" - } - } - ], - "scopes": [], - "decisionStrategy": "UNANIMOUS" - } + ] } ], "clientScopes": [ { - "id": "a894dbe0-76e7-4c22-b7b2-bd3f827e0ef5", + "id": "efb3ba35-32a8-42e2-a18c-a08d8648a645", "name": "role_list", "description": "SAML role list", "protocol": "saml", @@ -1023,7 +1010,7 @@ }, "protocolMappers": [ { - "id": "762589d9-35be-4ad7-bed4-4b718d6ef6ec", + "id": "e60166e0-7f32-4a57-a542-3528c09e947c", "name": "role list", "protocol": "saml", "protocolMapper": "saml-role-list-mapper", @@ -1037,81 +1024,17 @@ ] }, { - "id": "5a5ce089-2139-4d60-8d2a-fd198c5db2ec", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "927a5908-7652-4586-9b8a-eb5920ef4150", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "bf4c9750-93e5-434e-8845-adb5d545b462", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", + "id": "e2a67045-72e6-4c3f-b498-20089b36d557", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", "protocol": "openid-connect", "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "c3557b80-20cf-41cc-9732-9ebc2bd65e8a", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - }, - { - "id": "9b1e384f-9aed-4592-a40e-734030fdcfcb", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "userinfo.token.claim": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - } - ] + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } }, { - "id": "b19ae76e-fce0-4f6b-8d84-378f60d88f8d", + "id": "798a62ff-51bf-4872-80de-1b0b6f46ed9d", "name": "roles", "description": "OpenID Connect scope for add user roles to the access token", "protocol": "openid-connect", @@ -1122,7 +1045,15 @@ }, "protocolMappers": [ { - "id": "83b45cee-daa8-4a98-af4b-b9000f36f2fd", + "id": "f7e5d95a-a570-4971-8c9d-ec681071570c", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "d22b79cd-a184-42e4-9923-fc9d3c4041fa", "name": "client roles", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-client-role-mapper", @@ -1136,15 +1067,7 @@ } }, { - "id": "8695784f-2e6b-4571-982b-26b8ba72af98", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - }, - { - "id": "f36f78cb-da3f-4377-8b90-7d28078cc890", + "id": "ceeaafc0-dcab-4ce1-9c72-3044709d7be4", "name": "realm roles", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-realm-role-mapper", @@ -1160,107 +1083,75 @@ ] }, { - "id": "39baba4a-03aa-4309-8cd2-2591181f21ba", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "a2a22f05-cf5a-4206-9c7d-57fba22073c9", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "20187807-6f9e-4438-abec-164ca4e39520", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", + "id": "72852fcc-364d-4f5f-af44-28a13a477d8d", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" + "consent.screen.text": "${profileScopeConsentText}" }, "protocolMappers": [ { - "id": "ec0661bc-d266-4af6-aac4-a1753b1291d4", - "name": "phone number", + "id": "41eac4db-bb5a-49b8-87e4-0cf2f3eab306", + "name": "updated at", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", + "user.attribute": "updatedAt", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" + "claim.name": "updated_at", + "jsonType.label": "long" } }, { - "id": "a9b3a239-bc80-4067-b787-a2c3ca0d2ec4", - "name": "phone number verified", + "id": "dcd614c9-696c-4fad-a33e-664c228b6401", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "8f05f9d8-75f1-4be2-b8c8-df24d7499c1a", + "name": "birthdate", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", + "user.attribute": "birthdate", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" + "claim.name": "birthdate", + "jsonType.label": "String" } - } - ] - }, - { - "id": "e3d6fefe-3579-47a1-807d-64fcf7a87dcf", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - }, - { - "id": "e832567a-5345-4f8c-8b35-012f65396f67", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ + }, { - "id": "fd7a31da-915a-40ae-b633-393615ce2762", - "name": "updated at", + "id": "38c8437c-d727-4958-af1d-ff4df2b51f38", + "name": "family name", "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", + "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "updatedAt", + "user.attribute": "lastName", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "updated_at", + "claim.name": "family_name", "jsonType.label": "String" } }, { - "id": "75ffd8aa-4326-4923-bc3e-20b09bd875b0", + "id": "29067d4d-0bf4-4f90-ba5f-96ff7fcc00d1", "name": "middle name", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", @@ -1275,7 +1166,22 @@ } }, { - "id": "f2654fbe-5521-49e4-8e50-ca04651db68b", + "id": "2786ab57-a62e-4c2f-ad45-2da6bc3c6c26", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "a475bef6-0b47-4757-9d67-762d73450523", "name": "locale", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", @@ -1290,171 +1196,281 @@ } }, { - "id": "3cf479a7-f66d-4274-af23-ed1c7909b6e5", - "name": "profile", + "id": "fd6ddfa6-b50e-402b-a9fb-e9d9f43eb85b", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "bb1b0737-4a6e-4fd8-83b3-e37e7370efb4", + "name": "zoneinfo", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "profile", + "user.attribute": "zoneinfo", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "profile", + "claim.name": "zoneinfo", "jsonType.label": "String" } }, { - "id": "ef85df6a-0b3d-400b-b882-a2118ad44db5", - "name": "picture", + "id": "2a72a678-7b2e-498e-867a-49bc32c624a9", + "name": "website", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "picture", + "user.attribute": "website", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "picture", + "claim.name": "website", "jsonType.label": "String" } }, { - "id": "84451abc-bcbc-4451-9dcf-32836641765c", - "name": "birthdate", + "id": "0935915d-21bd-4a92-860d-d3d7e9c9e03e", + "name": "nickname", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "birthdate", + "user.attribute": "nickname", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "birthdate", + "claim.name": "nickname", "jsonType.label": "String" } }, { - "id": "67318bb2-5f53-4f75-a587-8f3319ebe843", - "name": "full name", + "id": "17957bd2-c4b2-4566-837c-a20bb8727a2e", + "name": "picture", "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", + "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", "id.token.claim": "true", "access.token.claim": "true", - "userinfo.token.claim": "true" + "claim.name": "picture", + "jsonType.label": "String" } }, { - "id": "820f7a36-03eb-4503-aa11-5742efe7390e", - "name": "username", + "id": "51074a6f-5d59-4b26-b62a-7fb710029ce7", + "name": "gender", "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", + "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "username", + "user.attribute": "gender", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "preferred_username", + "claim.name": "gender", "jsonType.label": "String" } }, { - "id": "233c42eb-c87d-4826-8bbc-4683c4f13a1a", - "name": "family name", + "id": "af54b82e-f672-4688-a440-25c6f2bd4e78", + "name": "profile", "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", + "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "lastName", + "user.attribute": "profile", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "family_name", + "claim.name": "profile", "jsonType.label": "String" } - }, + } + ] + }, + { + "id": "6ebb70be-c92a-454c-b759-2a094afe96a9", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ { - "id": "eb34b957-ea38-41ac-9199-697e227985e7", - "name": "gender", + "id": "0aa5d230-954d-42fa-8cfa-9ad77ab61128", + "name": "address", "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", + "protocolMapper": "oidc-address-mapper", "consentRequired": false, "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", "userinfo.token.claim": "true", - "user.attribute": "gender", + "user.attribute.street": "street", "id.token.claim": "true", + "user.attribute.region": "region", "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" + "user.attribute.locality": "locality" } - }, + } + ] + }, + { + "id": "915b3bf7-466f-4a44-b52e-ac4ccab2307e", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ { - "id": "50ba7956-d15f-4d8c-90aa-da136f09dcb2", - "name": "website", + "id": "df0e9106-4fd5-4cfa-9554-939902d3f72f", + "name": "groups", "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", + "protocolMapper": "oidc-usermodel-realm-role-mapper", "consentRequired": false, "config": { + "multivalued": "true", "userinfo.token.claim": "true", - "user.attribute": "website", + "user.attribute": "foo", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "website", + "claim.name": "groups", "jsonType.label": "String" } }, { - "id": "3f7b3d46-c9f0-43c2-90e9-e1a4874bdbcf", - "name": "given name", + "id": "e894e9a1-185b-40e4-b090-13a5dcb05e24", + "name": "upn", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "firstName", + "user.attribute": "username", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "given_name", + "claim.name": "upn", "jsonType.label": "String" } - }, + } + ] + }, + { + "id": "739c27ab-3430-4fda-8b9a-36f8c8e756bf", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ { - "id": "01ac5e4b-4945-4667-be10-d29dc5e6ad47", - "name": "nickname", + "id": "67843942-48bf-40f8-a044-329edd454384", + "name": "phone number", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "nickname", + "user.attribute": "phoneNumber", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "nickname", + "claim.name": "phone_number", "jsonType.label": "String" } }, { - "id": "cdb0ce02-86a3-4e76-84f7-167ede3e0ecf", - "name": "zoneinfo", + "id": "ebd29556-5a0d-429e-96ad-a5f09fe97f5e", + "name": "phone number verified", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", + "user.attribute": "phoneNumberVerified", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "97ee5b1a-63ee-4741-9b97-a933513934e2", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "67d4cdd7-13fc-4353-b32d-713b1a128d62", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "448b9039-0117-444d-a829-55ceaf53745a", + "name": "rayapiserver", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "gui.order": "", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "946a541e-d0de-4126-8dd3-fc55ac79f934", + "name": "rayapiserver", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "rayapiserver", + "id.token.claim": "true", + "access.token.claim": "true" } } ] }, { - "id": "04ef696e-7196-4c73-872d-10af8ebe4276", + "id": "81ea0941-13fd-4cfc-944a-7195e8636a35", "name": "email", "description": "OpenID Connect built-in scope: email", "protocol": "openid-connect", @@ -1465,7 +1481,7 @@ }, "protocolMappers": [ { - "id": "d8c56c76-ff18-4b37-b45a-237fcf8b2950", + "id": "f86d2554-a0e9-4efc-a39d-a5db3ac1b252", "name": "email verified", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", @@ -1480,7 +1496,7 @@ } }, { - "id": "d21719d1-850e-488f-a58d-a4e42c76f2a5", + "id": "a803daf8-d77d-4c4b-a5dc-b3e422b533ad", "name": "email", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-property-mapper", @@ -1495,6 +1511,27 @@ } } ] + }, + { + "id": "92a9ac5b-92c0-4516-abd2-28ab64dfe20f", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "30b096fc-c615-4ad0-a8bc-5ff4ae8179e6", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] } ], "defaultDefaultClientScopes": [ @@ -1502,7 +1539,8 @@ "profile", "email", "roles", - "web-origins" + "web-origins", + "acr" ], "defaultOptionalClientScopes": [ "offline_access", @@ -1532,19 +1570,7 @@ "components": { "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ { - "id": "632544be-5a8c-4e7e-b3c8-4cb5faedcf66", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": [ - "200" - ] - } - }, - { - "id": "3743b061-854b-43fd-8fcc-b687d015e9b5", + "id": "c7719aa8-cb64-41b0-9662-0ccf7cbd353f", "name": "Trusted Hosts", "providerId": "trusted-hosts", "subType": "anonymous", @@ -1559,7 +1585,26 @@ } }, { - "id": "7051cfe2-ab43-4faa-b40d-af6446b18167", + "id": "12c11176-207e-4409-a1b3-99aa1e1f8665", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "saml-user-attribute-mapper" + ] + } + }, + { + "id": "ac8014f0-e8db-46bf-80a9-e5c1b26e2a6f", "name": "Allowed Client Scopes", "providerId": "allowed-client-templates", "subType": "anonymous", @@ -1571,34 +1616,15 @@ } }, { - "id": "e0ec37dd-5965-48d3-81a6-3cb99629ccce", - "name": "Full Scope Disabled", - "providerId": "scope", + "id": "4cfbb988-5f41-428f-9413-b99237ba0ac9", + "name": "Consent Required", + "providerId": "consent-required", "subType": "anonymous", "subComponents": {}, "config": {} }, { - "id": "929899ea-bf1d-42b0-bd2a-9d1e432db44f", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-address-mapper", - "oidc-full-name-mapper", - "oidc-usermodel-property-mapper", - "saml-user-property-mapper", - "saml-role-list-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-user-attribute-mapper", - "oidc-usermodel-attribute-mapper" - ] - } - }, - { - "id": "d4a2ebb9-a3ae-44be-8678-3e00952c4b94", + "id": "d3c4f159-594b-485c-bc07-a2f3dfef45a1", "name": "Allowed Client Scopes", "providerId": "allowed-client-templates", "subType": "authenticated", @@ -1610,55 +1636,48 @@ } }, { - "id": "891f4a61-7f6e-4523-af0f-f11c55e9113c", - "name": "Consent Required", - "providerId": "consent-required", + "id": "9a7c9d59-e643-4df6-a13e-838d10e42251", + "name": "Full Scope Disabled", + "providerId": "scope", "subType": "anonymous", "subComponents": {}, "config": {} }, { - "id": "bd63ffe9-c748-4d6a-85ea-4677fa6260c7", + "id": "4c2e8ffe-9c73-4ec0-8bb8-3ef0bb97777c", "name": "Allowed Protocol Mapper Types", "providerId": "allowed-protocol-mappers", - "subType": "authenticated", + "subType": "anonymous", "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "saml-user-property-mapper", - "oidc-usermodel-attribute-mapper", - "saml-user-attribute-mapper", - "oidc-usermodel-property-mapper", "oidc-address-mapper", - "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", "saml-role-list-mapper", - "oidc-sha256-pairwise-sub-mapper" + "saml-user-property-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper" ] } - } - ], - "org.keycloak.userprofile.UserProfileProvider": [ - { - "id": "41f8cc61-7aeb-44b5-ad6b-990382a76fad", - "providerId": "declarative-user-profile", - "subComponents": {}, - "config": {} - } - ], - "org.keycloak.keys.KeyProvider": [ + }, { - "id": "acd1a5ea-6013-4353-beb1-4b8b00f50970", - "name": "aes-generated", - "providerId": "aes-generated", + "id": "b65257b4-45b5-4721-9d21-3339cf937211", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", "subComponents": {}, "config": { - "priority": [ - "100" + "max-clients": [ + "200" ] } - }, + } + ], + "org.keycloak.keys.KeyProvider": [ { - "id": "5b2b6b08-9d27-481a-9110-92ddba95a032", + "id": "de95f1d0-33eb-442f-bf2c-3684d19a083f", "name": "rsa-generated", "providerId": "rsa-generated", "subComponents": {}, @@ -1669,7 +1688,7 @@ } }, { - "id": "18f53e6d-9820-4064-ab92-4b4d59766399", + "id": "a426c7d7-a7de-4406-84e2-e73f9e854b4e", "name": "hmac-generated", "providerId": "hmac-generated", "subComponents": {}, @@ -1683,7 +1702,7 @@ } }, { - "id": "bb03bb29-3654-40bd-89cf-b97eb025fdf6", + "id": "55908bdd-159a-4c5c-b8ec-f4827ccde1d7", "name": "rsa-enc-generated", "providerId": "rsa-enc-generated", "subComponents": {}, @@ -1695,6 +1714,17 @@ "RSA-OAEP" ] } + }, + { + "id": "6368eb6c-d1d5-4d0a-b3d8-6c61f38808b2", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } } ] }, @@ -1702,7 +1732,7 @@ "supportedLocales": [], "authenticationFlows": [ { - "id": "9b9bd673-f110-4a4e-ac11-843a66e68b3a", + "id": "a9a29b0f-32e5-402f-b2cb-3d9759046ee1", "alias": "Account verification options", "description": "Method with which to verity the existing account", "providerId": "basic-flow", @@ -1714,21 +1744,21 @@ "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 20, + "autheticatorFlow": true, "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "6f683a06-820d-4ed6-9515-4df32eb81b2b", + "id": "387701d0-7ae1-41ec-873d-c2107cd90fbe", "alias": "Authentication Options", "description": "Authentication options.", "providerId": "basic-flow", @@ -1740,29 +1770,29 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "basic-auth-otp", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "auth-spnego", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, { - "id": "941107ba-0473-434a-b421-002e4f1a69d5", + "id": "38674533-c339-4627-b25b-5bb9081425f2", "alias": "Browser - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -1774,21 +1804,21 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "auth-otp-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, { - "id": "ca433bce-5571-4ea7-a244-83943d7bb32a", + "id": "42c97c74-7561-4c46-ab6e-a66f261baf0c", "alias": "Direct Grant - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -1800,21 +1830,21 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "direct-grant-validate-otp", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, { - "id": "4889a6d9-7042-4834-9a2c-25cfcf7ceee7", + "id": "412ebb8b-4794-4044-a116-3396afdbe29e", "alias": "First broker login - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -1826,21 +1856,21 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "auth-otp-form", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, { - "id": "89683d07-86f4-4742-9427-c503aec8f5b2", + "id": "644fdce3-0270-4a86-9469-fa77778c41e1", "alias": "Handle Existing Account", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId": "basic-flow", @@ -1852,21 +1882,21 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 20, + "autheticatorFlow": true, "flowAlias": "Account verification options", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "f335ed3a-4d73-4a01-a454-e8028a75268b", + "id": "4b20ed55-2f08-4a4e-b5a4-fb98372f3a4a", "alias": "Reset - Conditional OTP", "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId": "basic-flow", @@ -1878,21 +1908,21 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "reset-otp", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, { - "id": "64fe78c2-bb50-4085-8cf7-3b398dacf85f", + "id": "61b52ad6-199e-45df-9bfd-14affce3fc79", "alias": "User creation or linking", "description": "Flow for the existing/non-existing user alternatives", "providerId": "basic-flow", @@ -1905,21 +1935,21 @@ "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 20, + "autheticatorFlow": true, "flowAlias": "Handle Existing Account", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "f7570db5-d586-4f6d-ba46-9c01a7c75208", + "id": "fe8e510e-05ac-4034-a408-23e335c78eb4", "alias": "Verify Existing Account by Re-authentication", "description": "Reauthentication of existing account", "providerId": "basic-flow", @@ -1931,21 +1961,21 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 20, + "autheticatorFlow": true, "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "d540056f-3c26-4bed-aafc-4e8b15f7a9c8", + "id": "9c7c4ea7-f617-4295-bba3-54592a6fbfd1", "alias": "browser", "description": "browser based authentication", "providerId": "basic-flow", @@ -1957,37 +1987,37 @@ "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "auth-spnego", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "identity-provider-redirector", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 25, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 30, + "autheticatorFlow": true, "flowAlias": "forms", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "c68c0e86-4c68-4afb-9510-a13617149205", + "id": "75a73b01-3181-4ace-ba5c-d5765fa02ec6", "alias": "clients", "description": "Base authentication for clients", "providerId": "client-flow", @@ -1999,37 +2029,37 @@ "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "client-jwt", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "client-secret-jwt", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "client-x509", "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, { - "id": "c20894af-e1c1-4003-80e0-2a1d3006b31f", + "id": "daae1984-a148-4888-92bf-5b3787d7d9c1", "alias": "direct grant", "description": "OpenID Connect Resource Owner Grant", "providerId": "basic-flow", @@ -2041,29 +2071,29 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "direct-grant-validate-password", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 30, + "autheticatorFlow": true, "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "af8caa37-58f2-42fa-a65b-0bcf98bd9e6e", + "id": "f55cc7b8-781d-46fc-a89a-124b8a03a778", "alias": "docker auth", "description": "Used by Docker clients to authenticate against the IDP", "providerId": "basic-flow", @@ -2075,13 +2105,13 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, { - "id": "dd6c1bd1-b8d7-40b0-ab1a-db85cc3461d8", + "id": "0193e573-0c77-4103-9369-23dce00cc5d4", "alias": "first broker login", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId": "basic-flow", @@ -2094,21 +2124,21 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 20, + "autheticatorFlow": true, "flowAlias": "User creation or linking", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "3ce923e0-c377-4ccb-9f95-061aabc04bef", + "id": "0b4ac7e4-ca44-46b1-9939-9e98f90d888a", "alias": "forms", "description": "Username, password, otp and other auth forms.", "providerId": "basic-flow", @@ -2120,21 +2150,21 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 20, + "autheticatorFlow": true, "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "771ee177-6a6d-47d7-97e1-16bc28583d27", + "id": "4d56ed78-15ca-4457-a14b-1a9831204372", "alias": "http challenge", "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId": "basic-flow", @@ -2146,21 +2176,21 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 20, + "autheticatorFlow": true, "flowAlias": "Authentication Options", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "d6c90c8d-5f7a-4653-81d7-188074cc2ffe", + "id": "8a8d4c7d-a55f-494f-9219-056eefcc562f", "alias": "registration", "description": "registration flow", "providerId": "basic-flow", @@ -2172,14 +2202,14 @@ "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 10, + "autheticatorFlow": true, "flowAlias": "registration form", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "5aeb15de-bf71-4444-8867-62fd4d347e16", + "id": "afb69941-16d0-4536-a029-fc55a5cfd8a6", "alias": "registration form", "description": "registration form", "providerId": "form-flow", @@ -2191,37 +2221,37 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "registration-profile-action", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "registration-password-action", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 50, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "registration-recaptcha-action", "authenticatorFlow": false, "requirement": "DISABLED", "priority": 60, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, { - "id": "6192846f-4ecf-4fea-9fe6-2ce2d6cef01b", + "id": "9d7fc369-9439-43bf-9680-f9cf66e267b0", "alias": "reset credentials", "description": "Reset credentials for a user if they forgot their password or something", "providerId": "basic-flow", @@ -2233,37 +2263,37 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "reset-credential-email", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "reset-password", "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 40, + "autheticatorFlow": true, "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "5bf41189-9e07-4178-b203-542b69d751a6", + "id": "8ccf5a6b-ebf0-4ecb-a921-5661949b39dc", "alias": "saml ecp", "description": "SAML ECP Profile Authentication Flow", "providerId": "basic-flow", @@ -2275,22 +2305,22 @@ "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] } ], "authenticatorConfig": [ { - "id": "96c00f93-64c6-4e2f-b784-e03213c6582e", + "id": "b342e669-0e45-49a0-9059-5cb15936d6e5", "alias": "create unique user config", "config": { "require.password.update.after.registration": "false" } }, { - "id": "44007966-d7cc-47d3-b966-13e6278a878f", + "id": "113563dc-7a40-4146-a0dc-d02f24225499", "alias": "review profile config", "config": { "update.profile.on.first.login": "missing" @@ -2352,6 +2382,24 @@ "priority": 60, "config": {} }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, { "alias": "update_user_locale", "name": "Update User Locale", @@ -2379,9 +2427,10 @@ "parRequestUriLifespan": "60", "clientSessionMaxLifespan": "0", "clientOfflineSessionIdleTimeout": "0", - "cibaInterval": "5" + "cibaInterval": "5", + "realmReusableOtpCode": "false" }, - "keycloakVersion": "16.1.0", + "keycloakVersion": "20.0.3", "userManagedAccessAllowed": false, "clientProfiles": { "profiles": [] @@ -2389,4 +2438,4 @@ "clientPolicies": { "policies": [] } -} +} \ No newline at end of file From 7313a8575516c9af54e43e598d0fb1bb74059028 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Tue, 21 Mar 2023 17:58:00 -0400 Subject: [PATCH 13/15] Issue 217 | remove notebook --- docs/Untitled.ipynb | 92 --------------------------------------------- 1 file changed, 92 deletions(-) delete mode 100644 docs/Untitled.ipynb diff --git a/docs/Untitled.ipynb b/docs/Untitled.ipynb deleted file mode 100644 index a14cd8fec..000000000 --- a/docs/Untitled.ipynb +++ /dev/null @@ -1,92 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "7c048ff1-5bed-4972-900b-4cda6ac4aeed", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import IBMQ" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "bcf2a92a-3aef-42bf-990f-528964cd33c2", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ibmqfactory.load_account:WARNING:2023-03-17 18:27:17,504: Credentials are already in use. The existing account in the session will be replaced.\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "IBMQ.load_account() # Load account from disk" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "193e6835-a158-4b56-a36e-91154e36a77d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "IBMQ.get_provider(group=\"quantum-meeting\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28c2dcce-b2b9-426b-b1c6-dc01cf3fc9e0", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 8c27d3d998d1de357709a214e6ce8371a09c780d Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Tue, 21 Mar 2023 18:47:33 -0400 Subject: [PATCH 14/15] Issue 217 | Docs: tutorials update --- README.md | 165 +------------------- docker-compose.yml | 2 + docs/getting_started/01_intro_level_1.ipynb | 40 ++--- docs/getting_started/01_intro_level_2.ipynb | 75 +++++++-- docs/getting_started/01_intro_level_3.ipynb | 6 +- infrastructure/docker/Dockerfile-notebook | 1 + realm-export.json | 2 +- 7 files changed, 91 insertions(+), 200 deletions(-) diff --git a/README.md b/README.md index 3d72af5e3..2c31f4f56 100644 --- a/README.md +++ b/README.md @@ -33,173 +33,16 @@ You don't have to worry about configuration or scaling up computational resource ### Quickstart - - -
- 1. Hello World quickstart - -1 - Create program file `hello_qiskit.py` -```python -# hello_qiskit.py - -from qiskit import QuantumCircuit -from qiskit.primitives import Sampler - -circuit = QuantumCircuit(2) -circuit.h(0) -circuit.cx(0, 1) -circuit.measure_all() -circuit.draw() - -sampler = Sampler() - -quasi_dists = sampler.run(circuit).result().quasi_dists - -print(f"Quasi distribution: {quasi_dists[0]}") -``` - -2 - Run program file -```python -from quantum_serverless import QuantumServerless, Program -serverless = QuantumServerless(...) # serverless setup is provided by your admin or use docker compose (refer to all-in-one quickstart) -program = Program( - name="Hello Qiskit!", - entrypoint="hello_qiskit.py", - working_dir="./" -) - -job = serverless.run_program(program) -job.logs() -# 'Quasi distribution: {0: 0.4999999999999999, 3: 0.4999999999999999}\n' -``` -
- - -
- 2. All-in-one quickstart - -Steps -1. prepare infrastructure -2. write your program -3. run program - -#### Prepare infrastructure - -In the root folder of this project you can find `docker-compose.yml` -file, which is configured to run all necessary services for quickstart tutorials. - -Run in a root folder +1. Prepare local infrastructure ```shell docker-compose pull docker-compose up ``` -:memo: For more advanced ways to deploy the project you have the guide: -[Multi cloud deployment](https://qiskit-extensions.github.io/quantum-serverless/guides/08_multi_cloud_deployment.html). - -#### Write your program - -Create python file with necessary code. Let's call in `program.py` - -```python -# program.py -from qiskit import QuantumCircuit -from qiskit.circuit.random import random_circuit -from qiskit.quantum_info import SparsePauliOp -from qiskit.primitives import Estimator - -from quantum_serverless import QuantumServerless, run_qiskit_remote, get, put -from quantum_serverless.core.state import RedisStateHandler - -# 1. let's annotate out function to convert it -# to function that can be executed remotely -# using `run_qiskit_remote` decorator -@run_qiskit_remote() -def my_function(circuit: QuantumCircuit, obs: SparsePauliOp): - return Estimator().run([circuit], [obs]).result().values - - -# 2. Next let's create out serverless object to control -# where our remote function will be executed -serverless = QuantumServerless() - -# 2.1 (Optional) state handler to write/read results in/out of job -state_handler = RedisStateHandler("redis", 6379) - -circuits = [random_circuit(2, 2) for _ in range(3)] - -# 3. create serverless context -with serverless: - # 4. let's put some shared objects into remote storage that will be shared among all executions - obs_ref = put(SparsePauliOp(["ZZ"])) - - # 4. run our function and get back reference to it - # as now our function it remote one - function_reference = my_function(circuits[0], obs_ref) - - # 4.1 or we can run N of them in parallel (for all circuits) - function_references = [my_function(circ, obs_ref) for circ in circuits] - - # 5. to get results back from reference - # we need to call `get` on function reference - single_result = get(function_reference) - parallel_result = get(function_references) - print("Single execution:", single_result) - print("N parallel executions:", parallel_result) - - # 5.1 (Optional) write results to state. - state_handler.set("result", { - "status": "ok", - "single": single_result.tolist(), - "parallel_result": [entry.tolist() for entry in parallel_result] - }) -``` - -#### Run program - -Let's run our program now - -```python -from quantum_serverless import QuantumServerless, Program -from quantum_serverless.core.state import RedisStateHandler - -serverless = QuantumServerless({ - "providers": [{ - "name": "docker-compose", - "compute_resource": { - "name": "docker-compose", - "host": "localhost", # using our docker-compose infrastructure - } - }] -}) -serverless.set_provider("docker-compose") # set provider as docker-compose - -state_handler = RedisStateHandler("localhost", 6379) - -# create out program -program = Program( - name="my_program", - entrypoint="program.py" # set entrypoint as our program.py file -) - -job = serverless.run_program(program) - -job.status() -# - -job.logs() -# Single execution: [1.] -# N parallel executions: [array([1.]), array([0.]), array([-0.28650496])] - -state_handler.get("result") # (Optional) get written data -# {'status': 'ok', -# 'single': [1.0], -# 'parallel_result': [[1.0], [0.0], [-0.28650496]]} -``` - -
+2. Open jupyter notebook in browser at [http://localhost:8888/](http://localhost:8888/). Password for notebook is `123` +3. Explore 3 getting started tutorials. -For more detailed examples and explanations refer to [Beginners Guide](docs/beginners_guide.md) and [Getting started examples](docs/getting_started/) +For more detailed examples and explanations refer to [Beginners Guide](docs/beginners_guide.md), [Getting started examples](docs/getting_started/), [Guides](docs/guides) and [Tutorials](docs/tutorials). ---------------------------------------------------------------------------------------------------- diff --git a/docker-compose.yml b/docker-compose.yml index 193b67ab5..667e85427 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,8 @@ services: jupyter: container_name: qs-jupyter image: qiskit/quantum-serverless-notebook:nightly-py39 + volumes: + - ./docs/getting_started:/home/jovyan/ ports: - 8888:8888 environment: diff --git a/docs/getting_started/01_intro_level_1.ipynb b/docs/getting_started/01_intro_level_1.ipynb index 70313681d..8f5e204ca 100644 --- a/docs/getting_started/01_intro_level_1.ipynb +++ b/docs/getting_started/01_intro_level_1.ipynb @@ -9,7 +9,7 @@ "\n", "Let's write `Hello World` program using quantum serverless. \n", "\n", - "We will start with writing code for our program and saving it to [./source_files/gs_level_1.py](./source_files/gs_level_1.py) file. It will be simple hello world qiskit example.\n", + "We will start with writing code for our program and saving it to [./source_files/gs_level_1.py](./source_files/gs_level_1.py) file. Our program will be a Qiskit hello world example, which prepares a Bell state and then returns the measured probability distribution\n", "\n", "```python\n", "# source_files/gs_level_1.py\n", @@ -49,7 +49,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "acdec789-4967-48ee-8f6c-8d2b0ff57e91", "metadata": {}, "outputs": [ @@ -59,16 +59,16 @@ "" ] }, - "execution_count": 3, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "provider = GatewayProvider(\n", - " username=\"john\", # username is predefined in local docker setup\n", + " username=\"user\", # username is predefined in local docker setup\n", " password=\"password123\", # password is predefined in local docker setup\n", - " host=\"http://localhost:8000\", # address of provider\n", + " host=\"http://gateway:8000\", # address of provider\n", ")\n", "\n", "serverless = QuantumServerless(provider)\n", @@ -90,17 +90,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "d51df836-3f22-467c-b637-5803145d5d8a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 5, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -109,7 +109,7 @@ "from quantum_serverless import Program\n", "\n", "program = Program(\n", - " title=\"Getting started program level 1\", # you can choose any name you like. It is used to deferentiate if you have a lot of programs in array.\n", + " title=\"Getting started program level 1\", # you can choose any name you like. It is used to differentiate if you have a lot of programs in array.\n", " entrypoint=\"gs_level_1.py\", # entrypoint is file that will start your calculation\n", " working_dir=\"./source_files/\" # where you files are located. By default it is current directory.\n", ")\n", @@ -128,7 +128,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "cc7ccea6-bbae-4184-ba7f-67b6c20a0b0b", "metadata": {}, "outputs": [ @@ -138,7 +138,7 @@ "'SUCCEEDED'" ] }, - "execution_count": 6, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -149,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "ca76abfa-2ff5-425b-a225-058d91348e8b", "metadata": {}, "outputs": [ @@ -159,7 +159,7 @@ "'Quasi distribution: {0: 0.4999999999999999, 3: 0.4999999999999999}\\n'" ] }, - "execution_count": 7, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -179,17 +179,17 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "f942b76d-596c-4384-8f36-e5f73e72cefd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "1" + "'e4a801d2-f9eb-4392-b584-cbdd600755c8'" ] }, - "execution_count": 8, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -208,17 +208,17 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "45e2927f-655b-47a4-8003-f16e5ba0a1cd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 9, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -244,7 +244,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.13" + "version": "3.9.13" } }, "nbformat": 4, diff --git a/docs/getting_started/01_intro_level_2.ipynb b/docs/getting_started/01_intro_level_2.ipynb index 2b112eff2..cadc070de 100644 --- a/docs/getting_started/01_intro_level_2.ipynb +++ b/docs/getting_started/01_intro_level_2.ipynb @@ -98,7 +98,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "79434a17-1222-4d04-a81a-8140ed630ed6", "metadata": {}, "outputs": [], @@ -108,7 +108,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "b6ec8969-8c3d-4b7f-8c4c-adc6dbb9c59f", "metadata": {}, "outputs": [ @@ -118,16 +118,16 @@ "" ] }, - "execution_count": 4, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "provider = GatewayProvider(\n", - " username=\"john\", # username is predefined in local docker setup\n", + " username=\"user\", # username is predefined in local docker setup\n", " password=\"password123\", # password is predefined in local docker setup\n", - " host=\"http://localhost:8000\", # address of provider\n", + " host=\"http://gateway:8000\", # address of provider\n", ")\n", "\n", "serverless = QuantumServerless(provider)\n", @@ -146,17 +146,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "3ee09b31-4c7f-4ff3-af8f-294e4256793e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 5, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -180,7 +180,28 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, + "id": "420f2711-b8c6-4bf9-8651-c9d098348467", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'SUCCEEDED'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.status()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, "id": "7e3b0cc7-2f08-4b69-a266-bbbe4e9a6c59", "metadata": {}, "outputs": [ @@ -226,22 +247,24 @@ "metadata": {}, "source": [ "---\n", - "If you want to run this program with different bond length you can run it 3 times." + "If you want to run this program with different bond length you can run it 3 times. Programs are asynchronous, therefore each of instance of program will be running in parallel." ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "5f4d4317-bcc9-4e1a-942a-a38ca5331261", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[, , ]" + "[,\n", + " ,\n", + " ]" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -264,6 +287,27 @@ "jobs" ] }, + { + "cell_type": "code", + "execution_count": 14, + "id": "46fe3955-ac35-43d9-a5de-2a4e2cad1483", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RUNNING\n", + "RUNNING\n", + "RUNNING\n" + ] + } + ], + "source": [ + "for job in jobs:\n", + " print(job.status())" + ] + }, { "cell_type": "code", "execution_count": 10, @@ -363,7 +407,8 @@ "metadata": {}, "source": [ "---\n", - "Other way would be refactoring program file itself to accept list of bond length and run them in a loop inside a program" + "Other way would be refactoring program file itself to accept list of bond length and run them in a loop inside a program.\n", + "If you want 3 independent results, then running 3 programs would be a better fit. But if you want to do some postprocessing after execution of multiple function, then refactoring program file to run 3 function and postprocess them would be better choice. But at the end it all boils down to user preference." ] } ], @@ -383,7 +428,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.13" + "version": "3.9.13" } }, "nbformat": 4, diff --git a/docs/getting_started/01_intro_level_3.ipynb b/docs/getting_started/01_intro_level_3.ipynb index d8fe6b33f..7fb835b59 100644 --- a/docs/getting_started/01_intro_level_3.ipynb +++ b/docs/getting_started/01_intro_level_3.ipynb @@ -107,9 +107,9 @@ ], "source": [ "provider = GatewayProvider(\n", - " username=\"john\", # username is predefined in local docker setup\n", + " username=\"user\", # username is predefined in local docker setup\n", " password=\"password123\", # password is predefined in local docker setup\n", - " host=\"http://localhost:8000\", # address of provider\n", + " host=\"http://gateway:8000\", # address of provider\n", ")\n", "\n", "serverless = QuantumServerless(provider)\n", @@ -200,7 +200,7 @@ "id": "29336f0b-ffcf-4cdb-931c-11faf09f15ff", "metadata": {}, "source": [ - "With `job.result()` we can get saved results inside of our function back." + "With `job.result()` we can get saved results inside of our function back. `.result()` call will return you whatever you passed in `save_result` inside the program file, while `.logs()` will return everything that was logged by job (stdio, e.g prints)." ] }, { diff --git a/infrastructure/docker/Dockerfile-notebook b/infrastructure/docker/Dockerfile-notebook index 8dcfea1e0..2ba1b6955 100644 --- a/infrastructure/docker/Dockerfile-notebook +++ b/infrastructure/docker/Dockerfile-notebook @@ -10,5 +10,6 @@ RUN rm -r ./qs COPY --chown=$NB_UID:$NB_UID ./docs/tutorials/ ./serverless/tutorials/ COPY --chown=$NB_UID:$NB_UID ./docs/guides/ ./serverless/guides/ +COPY --chown=$NB_UID:$NB_UID ./docs/getting_started/ ./serverless/getting_started/ ENV JUPYTER_ENABLE_LAB=no diff --git a/realm-export.json b/realm-export.json index 1ce97b459..184ed061a 100644 --- a/realm-export.json +++ b/realm-export.json @@ -469,7 +469,7 @@ "groups": [] }, { - "username": "john", + "username": "user", "enabled": true, "email": "user@quatunserverless.org", "emailVerified": true, From d5b511330a06c00d79063ddb48fccfaf469bf717 Mon Sep 17 00:00:00 2001 From: Iskandar Sitdikov Date: Tue, 21 Mar 2023 19:07:51 -0400 Subject: [PATCH 15/15] Issue 217 | Docs: tutorials update --- docs/getting_started/01_intro_level_1.ipynb | 4 ++-- docs/getting_started/01_intro_level_2.ipynb | 4 ++-- docs/getting_started/01_intro_level_3.ipynb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/getting_started/01_intro_level_1.ipynb b/docs/getting_started/01_intro_level_1.ipynb index 8f5e204ca..7e435c30f 100644 --- a/docs/getting_started/01_intro_level_1.ipynb +++ b/docs/getting_started/01_intro_level_1.ipynb @@ -66,8 +66,8 @@ ], "source": [ "provider = GatewayProvider(\n", - " username=\"user\", # username is predefined in local docker setup\n", - " password=\"password123\", # password is predefined in local docker setup\n", + " username=\"user\", # this username has already been defined in local docker setup and does not need to be changed\n", + " password=\"password123\", # this password has already been defined in local docker setup and does not need to be changed\n", " host=\"http://gateway:8000\", # address of provider\n", ")\n", "\n", diff --git a/docs/getting_started/01_intro_level_2.ipynb b/docs/getting_started/01_intro_level_2.ipynb index cadc070de..4cc87c039 100644 --- a/docs/getting_started/01_intro_level_2.ipynb +++ b/docs/getting_started/01_intro_level_2.ipynb @@ -125,8 +125,8 @@ ], "source": [ "provider = GatewayProvider(\n", - " username=\"user\", # username is predefined in local docker setup\n", - " password=\"password123\", # password is predefined in local docker setup\n", + " username=\"user\", # this username has already been defined in local docker setup and does not need to be changed\n", + " password=\"password123\", # this password has already been defined in local docker setup and does not need to be changed\n", " host=\"http://gateway:8000\", # address of provider\n", ")\n", "\n", diff --git a/docs/getting_started/01_intro_level_3.ipynb b/docs/getting_started/01_intro_level_3.ipynb index 7fb835b59..28c9cdf0e 100644 --- a/docs/getting_started/01_intro_level_3.ipynb +++ b/docs/getting_started/01_intro_level_3.ipynb @@ -107,8 +107,8 @@ ], "source": [ "provider = GatewayProvider(\n", - " username=\"user\", # username is predefined in local docker setup\n", - " password=\"password123\", # password is predefined in local docker setup\n", + " username=\"user\", # this username has already been defined in local docker setup and does not need to be changed\n", + " password=\"password123\", # this password has already been defined in local docker setup and does not need to be changed\n", " host=\"http://gateway:8000\", # address of provider\n", ")\n", "\n",