Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic channel for custom channel platformsGeneric channels #2017

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ ibm_cloud_service = QiskitRuntimeService(channel="ibm_cloud", token="MY_IBM_CLOU

# For an IBM Quantum account.
ibm_quantum_service = QiskitRuntimeService(channel="ibm_quantum", token="MY_IBM_QUANTUM_TOKEN")

# For a generic/custom channel platform.
ibm_quantum_service = QiskitRuntimeService(channel="generic", token="MY_TOKEN", url="https://my.url:1234/")
```

## Primitives
Expand Down
81 changes: 75 additions & 6 deletions qiskit_ibm_runtime/accounts/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from ..utils.hgp import from_instance_format

from .exceptions import InvalidAccountError, CloudResourceNameResolutionError
from ..api.auth import QuantumAuth, CloudAuth
from ..api.auth import QuantumAuth, CloudAuth, GenericAuth
from ..utils import resolve_crn

AccountType = Optional[Literal["cloud", "legacy"]]
Expand All @@ -42,19 +42,20 @@ def __init__(
instance: Optional[str] = None,
proxies: Optional[ProxyConfiguration] = None,
verify: Optional[bool] = True,
url: Optional[str] = None,
):
"""Account constructor.

Args:
channel: Channel type, ``ibm_cloud`` or ``ibm_quantum``.
channel: Channel type, ``ibm_cloud`` or ``ibm_quantum`` or ``generic``.
token: Account token to use.
url: Authentication URL.
instance: Service instance to use.
proxies: Proxy configuration.
verify: Whether to verify server's TLS certificate.
"""
self.channel: str = None
self.url: str = None
self.url: str = url
self.token = token
self.instance = instance
self.proxies = proxies
Expand Down Expand Up @@ -118,10 +119,19 @@ def create_account(
verify=verify,
private_endpoint=private_endpoint,
)
elif channel == "generic":
return GenericAccount(
url=url,
token=token,
instance=instance,
proxies=proxies,
verify=verify,
private_endpoint=private_endpoint,
)
else:
raise InvalidAccountError(
f"Invalid `channel` value. Expected one of "
f"{['ibm_cloud', 'ibm_quantum']}, got '{channel}'."
f"{['ibm_cloud', 'ibm_quantum', 'generic']}, got '{channel}'."
)

def resolve_crn(self) -> None:
Expand Down Expand Up @@ -164,10 +174,10 @@ def validate(self) -> "Account":
@staticmethod
def _assert_valid_channel(channel: ChannelType) -> None:
"""Assert that the channel parameter is valid."""
if not (channel in ["ibm_cloud", "ibm_quantum"]):
if not (channel in ["ibm_cloud", "ibm_quantum", "generic"]):
raise InvalidAccountError(
f"Invalid `channel` value. Expected one of "
f"['ibm_cloud', 'ibm_quantum'], got '{channel}'."
f"['ibm_cloud', 'ibm_quantum', 'generic'], got '{channel}'."
)

@staticmethod
Expand Down Expand Up @@ -312,3 +322,62 @@ def _assert_valid_instance(instance: str) -> None:
"If using the ibm_quantum channel,",
"please specify the channel when saving your account with `channel = 'ibm_quantum'`.",
)

class GenericAccount(Account):
"""Class that represents an account with channel 'generic'."""

def __init__(
self,
token: str,
url: Optional[str] = None,
instance: Optional[str] = None,
proxies: Optional[ProxyConfiguration] = None,
verify: Optional[bool] = True,
private_endpoint: Optional[bool] = False,
):
"""Account constructor.

Args:
token: Account token to use.
url: Authentication URL.
instance: Service instance to use.
proxies: Proxy configuration.
verify: Whether to verify server's TLS certificate.
private_endpoint: Connect to private API URL.
"""
super().__init__(token, instance, proxies, verify)
self.channel = "generic"
self.url = url
self.private_endpoint = private_endpoint

def get_auth_handler(self) -> AuthBase:
"""Returns the generic authentication handler."""
return GenericAuth(api_key=self.token, crn=self.instance)

def resolve_crn(self) -> None:
"""Resolves the corresponding unique Cloud Resource Name (CRN) for the given non-unique service
instance name and updates the ``instance`` attribute accordingly.
"""

crn = resolve_crn(
channel="generic",
url=self.url,
token=self.token,
instance=self.instance,
)
if len(crn) > 1:
# handle edge-case where multiple service instances with the same name exist
logger.warning(
"Multiple CRN values found for service name %s: %s. Using %s.",
self.instance,
crn,
crn[0],
)

# overwrite with CRN value
self.instance = crn[0]

@staticmethod
def _assert_valid_instance(instance: str) -> None:
"""Assert that the instance name is valid, if given. As it is an optional parameter, passes."""
pass
30 changes: 30 additions & 0 deletions qiskit_ibm_runtime/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,33 @@ def __call__(self, r: PreparedRequest) -> PreparedRequest:
def get_headers(self) -> Dict:
"""Return authorization information to be stored in header."""
return {"X-Access-Token": self.access_token}


class GenericAuth(AuthBase):
"""Attaches Generic Authentication to the given Request object.\n
"""

def __init__(self, api_key: str, crn: str):
self.api_key = api_key
self.crn = crn

def __eq__(self, other: object) -> bool:
if isinstance(other, GenericAuth):
return all(
[
self.api_key == other.api_key,
self.crn == other.crn,
]
)
return False

def __call__(self, r: PreparedRequest) -> PreparedRequest:
r.headers.update(self.get_headers())
return r

def get_headers(self) -> Dict:
"""Return authorization information to be stored in header."""
if self.crn is None:
return {"Authorization": f"apikey {self.api_key}"}
else:
return {"Service-CRN": self.crn, "Authorization": f"apikey {self.api_key}"}
15 changes: 10 additions & 5 deletions qiskit_ibm_runtime/api/client_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ..proxies import ProxyConfiguration

from ..utils import default_runtime_url_resolver
from ..api.auth import QuantumAuth, CloudAuth
from ..api.auth import QuantumAuth, CloudAuth, GenericAuth

TEMPLATE_IBM_HUBS = "{prefix}/Network/{hub}/Groups/{group}/Projects/{project}"
"""str: Template for creating an IBM Quantum URL with hub/group/project information."""
Expand Down Expand Up @@ -59,16 +59,21 @@ def __init__(
url_resolver = default_runtime_url_resolver
self.url_resolver = url_resolver

def get_auth_handler(self) -> Union[CloudAuth, QuantumAuth]:
def get_auth_handler(self) -> Union[CloudAuth, QuantumAuth, GenericAuth]:
"""Returns the respective authentication handler."""
if self.channel == "ibm_cloud":
return CloudAuth(api_key=self.token, crn=self.instance)

return QuantumAuth(access_token=self.token)
elif self.channel == "generic":
return GenericAuth(api_key=self.token, crn=self.instance)
else:
return QuantumAuth(access_token=self.token)

def get_runtime_api_base_url(self) -> str:
"""Returns the Runtime API base url."""
return self.url_resolver(self.url, self.instance, self.private_endpoint)
if self.channel == "generic":
return self.url
else:
return self.url_resolver(self.url, self.instance, self.private_endpoint)

def connection_parameters(self) -> Dict[str, Any]:
"""Construct connection related parameters.
Expand Down
10 changes: 7 additions & 3 deletions qiskit_ibm_runtime/qiskit_runtime_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ def __init__(
if self._channel == "ibm_cloud":
self._api_client = RuntimeClient(self._client_params)
self._backend_allowed_list = self._discover_cloud_backends()
elif self._channel == "generic":
self._api_client = RuntimeClient(self._client_params)
self._backend_allowed_list = self._discover_cloud_backends() # same way as in ibm_cloud
else:
auth_client = self._authenticate_ibm_quantum_account(self._client_params)
# Update client parameters to use authenticated values.
Expand Down Expand Up @@ -213,8 +216,8 @@ def _discover_account(
)
account = AccountManager.get(filename=filename, name=name)
elif channel:
if channel and channel not in ["ibm_cloud", "ibm_quantum"]:
raise ValueError("'channel' can only be 'ibm_cloud' or 'ibm_quantum'")
if channel and channel not in ["ibm_cloud", "ibm_quantum", "generic"]:
raise ValueError("'channel' can only be 'ibm_cloud', 'ibm_quantum' or 'generic'")
if token:
account = Account.create_account(
channel=channel,
Expand Down Expand Up @@ -246,7 +249,8 @@ def _discover_account(
account.verify = verify

# resolve CRN if needed
self._resolve_crn(account)
if(not channel == 'generic'):
self._resolve_crn(account)

# ensure account is valid, fail early if not
account.validate()
Expand Down
4 changes: 2 additions & 2 deletions qiskit_ibm_runtime/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ def get_resource_controller_api_url(cloud_url: str) -> str:

def resolve_crn(channel: str, url: str, instance: str, token: str) -> List[str]:
"""Resolves the Cloud Resource Name (CRN) for the given cloud account."""
if channel != "ibm_cloud":
raise ValueError("CRN value can only be resolved for cloud accounts.")
if channel not in["ibm_cloud", "generic"]:
raise ValueError("CRN value can only be resolved for cloud and generic accounts.")

if is_crn(instance):
# no need to resolve CRN value by name
Expand Down
8 changes: 8 additions & 0 deletions release-notes/unreleased/todo.feat.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Create a new channel `generic`. This channel allows to use an alternative
and custom channel platform implementation, i.e. neither IBM Quantum Platform
nor IBM Cloud. The url parameter can be used to describe how the channel
platform is reached. It will not be further modified as with other channel
options.
While `token` and `instance` can be used if the HTTP headers match what the
custom channel platform expects, additional headers can be set through the
environment variable `QISKIT_IBM_RUNTIME_CUSTOM_CLIENT_APP_HEADER`.