Skip to content

Commit 497fb0b

Browse files
Tansitopaaragon
andauthored
IBM Cloud Integration (#1593)
* IBM Cloud authentication process (#1592) * created the ibm_cloud service to authenticate with * preparing the tests * added tests for ibm cloud authentication process * fix lint * fix authentication logger * Local authentication refactor (#1595) * mock backend renamed * created local service * added the channel as a parameter in the use case * improved the tests to verify permissions in groups * fix bug creating the provider * Channel implementation in client (#1596) * Channel implementation in client * fix bug with settings configuration * use get_headers in serverless client * get_headers as static method * include instance in files client * move get_headers to all the requests * moved get_headers to utils * updated docstrings * added the same param to ibm serverless client * Configure the IBM Cloud env vars for Functions (#1597) * fixed a bug in the client uploading functions * setup channel and instance in the env vars * updated references to old env vars * use the new env vars in the client * fix test * fix lint and black * fix tests * fix enum value * tests updated * fix black and lint * Improve the logic to manage authentication errors (#1599) * improve logic with authentication failed exception * fix linter * implement the headers for 401 exception * Empty commit * update the interface for the channel * restart docker-compose configuration * restart docker-compose configuration * Fix/save account (#1600) * fix: IBMServerlessClient save_account * fix lint * fix docstring * apply partial review suggestions * configure environment variables in helm (#1603) * New Group creation process for IBM Cloud (#1602) * Store the access group separately from the id * restart docker-compose configuration * move custom group to inheritance one * updated tests --------- Co-authored-by: Pablo Aragón <pablo.aragon@ibm.com>
1 parent 42af800 commit 497fb0b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1170
-236
lines changed

charts/qiskit-serverless/charts/gateway/templates/deployment.yaml

+6-2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ spec:
109109
value: {{ .Values.application.auth.token.url | quote }}
110110
- name: SETTINGS_TOKEN_AUTH_VERIFICATION_FIELD
111111
value: {{ .Values.application.auth.token.verificationField | quote }}
112+
- name: IAM_IBM_CLOUD_BASE_URL
113+
value: {{ .Values.application.auth.token.iamUrl | quote }}
114+
- name: RESOURCE_CONTROLLER_IBM_CLOUD_BASE_URL
115+
value: {{ .Values.application.auth.token.resourceControllerUrl | quote }}
116+
- name: RESOURCE_PLANS_ID_ALLOWED
117+
value: {{ .Values.application.auth.token.resourcePlansAllowed | quote }}
112118
- name: SETTINGS_AUTH_MOCKPROVIDER_REGISTRY
113119
value: {{ .Values.application.authMockproviderRegistry }}
114120
- name: RAY_CLUSTER_WORKER_REPLICAS
@@ -156,8 +162,6 @@ spec:
156162
value: {{ .Values.application.ray.openTelemetryCollector.host }}:{{ .Values.application.ray.openTelemetryCollector.port }}
157163
- name: OTEL_EXPORTER_OTLP_TRACES_INSECURE
158164
value: {{ .Values.application.ray.openTelemetryCollector.insecure | quote }}
159-
- name: QISKIT_IBM_CHANNEL
160-
value: {{ .Values.application.qiskitRuntime.channel }}
161165
- name: QISKIT_IBM_URL
162166
value: {{ .Values.application.qiskitRuntime.url }}
163167
- name: IQP_QCON_API_BASE_URL

charts/qiskit-serverless/charts/gateway/values.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ application:
4444
keepClusterOnComplete: False
4545
programTimeoutDays: 14
4646
qiskitRuntime:
47-
channel: "ibm_quantum"
4847
url: "https://auth.quantum-computing.ibm.com/api"
4948
iqpQcon:
5049
url: "https://api-qcon.quantum.ibm.com/api"

client/qiskit_serverless/core/client.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ class BaseClient(JobService, RunService, JsonSerializable, ABC):
5757
"""
5858

5959
def __init__( # pylint: disable=too-many-positional-arguments
60-
self, name: str, host: Optional[str] = None, token: Optional[str] = None
60+
self,
61+
name: str,
62+
host: Optional[str] = None,
63+
token: Optional[str] = None,
64+
instance: Optional[str] = None,
6165
):
6266
"""
6367
Initialize a BaseClient instance.
@@ -66,10 +70,12 @@ def __init__( # pylint: disable=too-many-positional-arguments
6670
name: name of client
6771
host: host of client a.k.a managers host
6872
token: authentication token for manager
73+
instance: IBM Cloud CRN
6974
"""
7075
self.name = name
7176
self.host = host
7277
self.token = token
78+
self.instance = instance
7379

7480
@classmethod
7581
@abstractmethod

client/qiskit_serverless/core/clients/serverless_client.py

+96-28
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
)
5151
from qiskit_serverless.core.client import BaseClient
5252
from qiskit_serverless.core.decorators import trace_decorator_factory
53+
from qiskit_serverless.core.enums import Channel
5354
from qiskit_serverless.core.files import GatewayFilesClient
5455
from qiskit_serverless.core.job import (
5556
Job,
@@ -62,6 +63,7 @@
6263
)
6364

6465
from qiskit_serverless.exception import QiskitServerlessException
66+
from qiskit_serverless.utils.http import get_headers
6567
from qiskit_serverless.utils.json import (
6668
safe_json_request_as_dict,
6769
safe_json_request_as_list,
@@ -95,6 +97,8 @@ def __init__( # pylint: disable=too-many-positional-arguments
9597
host: Optional[str] = None,
9698
version: Optional[str] = None,
9799
token: Optional[str] = None,
100+
instance: Optional[str] = None,
101+
channel: str = Channel.IBM_QUANTUM.value,
98102
verbose: bool = False,
99103
):
100104
"""
@@ -105,6 +109,8 @@ def __init__( # pylint: disable=too-many-positional-arguments
105109
host: host of gateway
106110
version: version of gateway
107111
token: authorization token
112+
instance: IBM Cloud CRN
113+
channel: identifies the method to use to authenticate the user
108114
"""
109115
name = name or "gateway-client"
110116
host = host or os.environ.get(ENV_GATEWAY_PROVIDER_HOST)
@@ -121,30 +127,47 @@ def __init__( # pylint: disable=too-many-positional-arguments
121127
"Authentication credentials must be provided in form of `token`."
122128
)
123129

124-
super().__init__(name, host, token)
130+
try:
131+
channel_enum = Channel(channel)
132+
except ValueError as error:
133+
raise QiskitServerlessException(
134+
"Your channel value is not correct. Use one of the available channels: "
135+
f"{Channel.LOCAL.value}, {Channel.IBM_QUANTUM.value}, {Channel.IBM_CLOUD.value}"
136+
) from error
137+
138+
if channel_enum is Channel.IBM_CLOUD and instance is None:
139+
raise QiskitServerlessException(
140+
"Authentication with IBM Cloud requires to pass the CRN as an instance."
141+
)
142+
143+
super().__init__(name, host, token, instance)
125144
self.verbose = verbose
126145
self.version = version
127-
self._verify_token(token)
146+
self._verify_credentials()
128147

129-
self._files_client = GatewayFilesClient(self.host, self.token, self.version)
148+
self._files_client = GatewayFilesClient(
149+
self.host, self.token, self.version, self.instance
150+
)
130151

131152
@classmethod
132153
def from_dict(cls, dictionary: dict):
133154
return ServerlessClient(**dictionary)
134155

135-
def _verify_token(self, token: str):
136-
"""Verify token."""
156+
def _verify_credentials(self):
157+
"""Verify against the API that the credentials are correct."""
137158
try:
138159
safe_json_request(
139160
request=lambda: requests.get(
140161
url=f"{self.host}/api/v1/programs/",
141-
headers={"Authorization": f"Bearer {token}"},
162+
headers=get_headers(token=self.token, instance=self.instance),
142163
timeout=REQUESTS_TIMEOUT,
143164
),
144165
verbose=self.verbose,
145166
)
146167
except QiskitServerlessException as reason:
147-
raise QiskitServerlessException("Cannot verify token.") from reason
168+
raise QiskitServerlessException(
169+
"Credentials couldn't be verified."
170+
) from reason
148171

149172
####################
150173
####### JOBS #######
@@ -161,7 +184,7 @@ def jobs(self, **kwargs) -> List[Job]:
161184
request=lambda: requests.get(
162185
f"{self.host}/api/{self.version}/jobs/",
163186
params=kwargs,
164-
headers={"Authorization": f"Bearer {self.token}"},
187+
headers=get_headers(token=self.token, instance=self.instance),
165188
timeout=REQUESTS_TIMEOUT,
166189
)
167190
)
@@ -200,7 +223,7 @@ def provider_jobs(self, function: QiskitFunction, **kwargs) -> List[Job]:
200223
request=lambda: requests.get(
201224
f"{self.host}/api/{self.version}/jobs/provider/",
202225
params=kwargs,
203-
headers={"Authorization": f"Bearer {self.token}"},
226+
headers=get_headers(token=self.token, instance=self.instance),
204227
timeout=REQUESTS_TIMEOUT,
205228
)
206229
)
@@ -216,7 +239,7 @@ def job(self, job_id: str) -> Optional[Job]:
216239
response_data = safe_json_request_as_dict(
217240
request=lambda: requests.get(
218241
url,
219-
headers={"Authorization": f"Bearer {self.token}"},
242+
headers=get_headers(token=self.token, instance=self.instance),
220243
timeout=REQUESTS_TIMEOUT,
221244
)
222245
)
@@ -266,7 +289,7 @@ def run(
266289
request=lambda: requests.post(
267290
url=url,
268291
json=data,
269-
headers={"Authorization": f"Bearer {self.token}"},
292+
headers=get_headers(token=self.token, instance=self.instance),
270293
timeout=REQUESTS_TIMEOUT,
271294
)
272295
)
@@ -282,7 +305,7 @@ def status(self, job_id: str):
282305
request=lambda: requests.get(
283306
f"{self.host}/api/{self.version}/jobs/{job_id}/",
284307
params={"with_result": "false"},
285-
headers={"Authorization": f"Bearer {self.token}"},
308+
headers=get_headers(token=self.token, instance=self.instance),
286309
timeout=REQUESTS_TIMEOUT,
287310
)
288311
)
@@ -302,7 +325,7 @@ def stop(self, job_id: str, service: Optional[QiskitRuntimeService] = None):
302325
response_data = safe_json_request_as_dict(
303326
request=lambda: requests.post(
304327
f"{self.host}/api/{self.version}/jobs/{job_id}/stop/",
305-
headers={"Authorization": f"Bearer {self.token}"},
328+
headers=get_headers(token=self.token, instance=self.instance),
306329
timeout=REQUESTS_TIMEOUT,
307330
json=data,
308331
)
@@ -315,7 +338,7 @@ def result(self, job_id: str):
315338
response_data = safe_json_request_as_dict(
316339
request=lambda: requests.get(
317340
f"{self.host}/api/{self.version}/jobs/{job_id}/",
318-
headers={"Authorization": f"Bearer {self.token}"},
341+
headers=get_headers(token=self.token, instance=self.instance),
319342
timeout=REQUESTS_TIMEOUT,
320343
)
321344
)
@@ -328,7 +351,7 @@ def logs(self, job_id: str):
328351
response_data = safe_json_request_as_dict(
329352
request=lambda: requests.get(
330353
f"{self.host}/api/{self.version}/jobs/{job_id}/logs/",
331-
headers={"Authorization": f"Bearer {self.token}"},
354+
headers=get_headers(token=self.token, instance=self.instance),
332355
timeout=REQUESTS_TIMEOUT,
333356
)
334357
)
@@ -368,12 +391,22 @@ def upload(self, program: QiskitFunction) -> Optional[RunnableQiskitFunction]:
368391
if program.image is not None:
369392
# upload function with custom image
370393
function_uploaded = _upload_with_docker_image(
371-
program=program, url=url, token=self.token, span=span, client=self
394+
program=program,
395+
url=url,
396+
token=self.token,
397+
span=span,
398+
client=self,
399+
instance=self.instance,
372400
)
373401
elif program.entrypoint is not None:
374402
# upload funciton with artifact
375403
function_uploaded = _upload_with_artifact(
376-
program=program, url=url, token=self.token, span=span, client=self
404+
program=program,
405+
url=url,
406+
token=self.token,
407+
span=span,
408+
client=self,
409+
instance=self.instance,
377410
)
378411
else:
379412
raise QiskitServerlessException(
@@ -388,7 +421,7 @@ def functions(self, **kwargs) -> List[RunnableQiskitFunction]:
388421
response_data = safe_json_request_as_list(
389422
request=lambda: requests.get(
390423
f"{self.host}/api/{self.version}/programs",
391-
headers={"Authorization": f"Bearer {self.token}"},
424+
headers=get_headers(token=self.token, instance=self.instance),
392425
params=kwargs,
393426
timeout=REQUESTS_TIMEOUT,
394427
)
@@ -417,7 +450,7 @@ def function(
417450
response_data = safe_json_request_as_dict(
418451
request=lambda: requests.get(
419452
f"{self.host}/api/{self.version}/programs/get_by_title/{title}",
420-
headers={"Authorization": f"Bearer {self.token}"},
453+
headers=get_headers(token=self.token, instance=self.instance),
421454
params={"provider": provider},
422455
timeout=REQUESTS_TIMEOUT,
423456
)
@@ -511,7 +544,13 @@ class IBMServerlessClient(ServerlessClient):
511544
client = IBMServerlessClient(token=<INSERT_IBM_QUANTUM_TOKEN>)
512545
"""
513546

514-
def __init__(self, token: Optional[str] = None, name: Optional[str] = None):
547+
def __init__(
548+
self,
549+
token: Optional[str] = None,
550+
name: Optional[str] = None,
551+
instance: Optional[str] = None,
552+
channel: str = Channel.IBM_QUANTUM.value,
553+
):
515554
"""
516555
Initialize a client with access to an IBMQ-provided remote cluster.
517556
@@ -528,15 +567,24 @@ def __init__(self, token: Optional[str] = None, name: Optional[str] = None):
528567
Args:
529568
token: IBM quantum token
530569
name: Name of the account to load
570+
instance: IBM Cloud CRN
571+
channel: identifies the method to use to authenticate the user
531572
"""
532573
token = token or QiskitRuntimeService(name=name).active_account().get("token")
533-
super().__init__(token=token, host=IBM_SERVERLESS_HOST_URL)
574+
super().__init__(
575+
channel=channel,
576+
token=token,
577+
instance=instance,
578+
host=IBM_SERVERLESS_HOST_URL,
579+
)
534580

535581
@staticmethod
536582
def save_account(
537583
token: Optional[str] = None,
538584
name: Optional[str] = None,
539585
overwrite: Optional[bool] = False,
586+
instance: Optional[str] = None,
587+
channel: str = Channel.IBM_QUANTUM.value,
540588
) -> None:
541589
"""
542590
Save the account to disk for future use.
@@ -545,12 +593,25 @@ def save_account(
545593
token: IBM Quantum API token
546594
name: Name of the account to save
547595
overwrite: ``True`` if the existing account is to be overwritten
596+
instance: IBM Cloud CRN
597+
channel: identifies the method to use to authenticate the user
548598
"""
549-
QiskitRuntimeService.save_account(token=token, name=name, overwrite=overwrite)
599+
QiskitRuntimeService.save_account(
600+
token=token,
601+
name=name,
602+
overwrite=overwrite,
603+
instance=instance,
604+
channel=channel,
605+
)
550606

551607

552-
def _upload_with_docker_image(
553-
program: QiskitFunction, url: str, token: str, span: Any, client: RunService
608+
def _upload_with_docker_image( # pylint: disable=too-many-positional-arguments
609+
program: QiskitFunction,
610+
url: str,
611+
token: str,
612+
span: Any,
613+
client: RunService,
614+
instance: Optional[str],
554615
) -> RunnableQiskitFunction:
555616
"""Uploads function with custom docker image.
556617
@@ -559,6 +620,7 @@ def _upload_with_docker_image(
559620
url (str): upload gateway url
560621
token (str): auth token
561622
span (Any): tracing span
623+
instance (Optional[str]): IBM Cloud crn
562624
563625
Returns:
564626
str: uploaded function name
@@ -575,7 +637,7 @@ def _upload_with_docker_image(
575637
"env_vars": json.dumps(program.env_vars or {}),
576638
"description": program.description,
577639
},
578-
headers={"Authorization": f"Bearer {token}"},
640+
headers=get_headers(token=token, instance=instance),
579641
timeout=REQUESTS_TIMEOUT,
580642
)
581643
)
@@ -587,8 +649,13 @@ def _upload_with_docker_image(
587649
return RunnableQiskitFunction.from_json(response_data)
588650

589651

590-
def _upload_with_artifact(
591-
program: QiskitFunction, url: str, token: str, span: Any, client: RunService
652+
def _upload_with_artifact( # pylint: disable=too-many-positional-arguments
653+
program: QiskitFunction,
654+
url: str,
655+
token: str,
656+
span: Any,
657+
client: RunService,
658+
instance: Optional[str],
592659
) -> RunnableQiskitFunction:
593660
"""Uploads function with artifact.
594661
@@ -597,6 +664,7 @@ def _upload_with_artifact(
597664
url (str): endpoint for gateway upload
598665
token (str): auth token
599666
span (Any): tracing span
667+
instance (Optional[str]): IBM Cloud crn
600668
601669
Raises:
602670
QiskitServerlessException: if no entrypoint or size of artifact is too large.
@@ -645,7 +713,7 @@ def _upload_with_artifact(
645713
"description": program.description,
646714
},
647715
files={"artifact": file},
648-
headers={"Authorization": f"Bearer {token}"},
716+
headers=get_headers(token=token, instance=instance),
649717
timeout=REQUESTS_TIMEOUT,
650718
)
651719
)

client/qiskit_serverless/core/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
# auth
3737
ENV_JOB_GATEWAY_TOKEN = "ENV_JOB_GATEWAY_TOKEN"
38+
ENV_JOB_GATEWAY_INSTANCE = "ENV_JOB_GATEWAY_INSTANCE"
3839
ENV_JOB_GATEWAY_HOST = "ENV_JOB_GATEWAY_HOST"
3940
ENV_JOB_ID_GATEWAY = "ENV_JOB_ID_GATEWAY"
4041
ENV_JOB_ARGUMENTS = "ENV_JOB_ARGUMENTS"

0 commit comments

Comments
 (0)