mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-15 19:38:11 +00:00
allow deployment to non-default subscriptions (#774)
This commit is contained in:
@ -122,7 +122,9 @@ class Client:
|
|||||||
log_service_principal: bool,
|
log_service_principal: bool,
|
||||||
multi_tenant_domain: str,
|
multi_tenant_domain: str,
|
||||||
upgrade: bool,
|
upgrade: bool,
|
||||||
|
subscription_id: Optional[str],
|
||||||
):
|
):
|
||||||
|
self.subscription_id = subscription_id
|
||||||
self.resource_group = resource_group
|
self.resource_group = resource_group
|
||||||
self.arm_template = arm_template
|
self.arm_template = arm_template
|
||||||
self.location = location
|
self.location = location
|
||||||
@ -171,11 +173,16 @@ class Client:
|
|||||||
self.workbook_data = json.load(f)
|
self.workbook_data = json.load(f)
|
||||||
|
|
||||||
def get_subscription_id(self) -> str:
|
def get_subscription_id(self) -> str:
|
||||||
|
if self.subscription_id:
|
||||||
|
return self.subscription_id
|
||||||
profile = get_cli_profile()
|
profile = get_cli_profile()
|
||||||
return cast(str, profile.get_subscription_id())
|
self.subscription_id = cast(str, profile.get_subscription_id())
|
||||||
|
return self.subscription_id
|
||||||
|
|
||||||
def get_location_display_name(self) -> str:
|
def get_location_display_name(self) -> str:
|
||||||
location_client = get_client_from_cli_profile(SubscriptionClient)
|
location_client = get_client_from_cli_profile(
|
||||||
|
SubscriptionClient, subscription_id=self.get_subscription_id()
|
||||||
|
)
|
||||||
locations = location_client.subscriptions.list_locations(
|
locations = location_client.subscriptions.list_locations(
|
||||||
self.get_subscription_id()
|
self.get_subscription_id()
|
||||||
)
|
)
|
||||||
@ -194,7 +201,9 @@ class Client:
|
|||||||
with open(self.arm_template, "r") as handle:
|
with open(self.arm_template, "r") as handle:
|
||||||
arm = json.load(handle)
|
arm = json.load(handle)
|
||||||
|
|
||||||
client = get_client_from_cli_profile(ResourceManagementClient)
|
client = get_client_from_cli_profile(
|
||||||
|
ResourceManagementClient, subscription_id=self.get_subscription_id()
|
||||||
|
)
|
||||||
providers = {x.namespace: x for x in client.providers.list()}
|
providers = {x.namespace: x for x in client.providers.list()}
|
||||||
|
|
||||||
unsupported = []
|
unsupported = []
|
||||||
@ -233,7 +242,7 @@ class Client:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def create_password(self, object_id: UUID) -> Tuple[str, str]:
|
def create_password(self, object_id: UUID) -> Tuple[str, str]:
|
||||||
return add_application_password(object_id)
|
return add_application_password(object_id, self.get_subscription_id())
|
||||||
|
|
||||||
def setup_rbac(self) -> None:
|
def setup_rbac(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -245,7 +254,9 @@ class Client:
|
|||||||
logger.info("using existing client application")
|
logger.info("using existing client application")
|
||||||
return
|
return
|
||||||
|
|
||||||
client = get_client_from_cli_profile(GraphRbacManagementClient)
|
client = get_client_from_cli_profile(
|
||||||
|
GraphRbacManagementClient, subscription_id=self.get_subscription_id()
|
||||||
|
)
|
||||||
logger.info("checking if RBAC already exists")
|
logger.info("checking if RBAC already exists")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -400,7 +411,10 @@ class Client:
|
|||||||
"subscription, creating a new one"
|
"subscription, creating a new one"
|
||||||
)
|
)
|
||||||
app_info = register_application(
|
app_info = register_application(
|
||||||
"onefuzz-cli", self.application_name, OnefuzzAppRole.CliClient
|
"onefuzz-cli",
|
||||||
|
self.application_name,
|
||||||
|
OnefuzzAppRole.CliClient,
|
||||||
|
self.get_subscription_id(),
|
||||||
)
|
)
|
||||||
if self.multi_tenant_domain:
|
if self.multi_tenant_domain:
|
||||||
authority = COMMON_AUTHORITY
|
authority = COMMON_AUTHORITY
|
||||||
@ -429,7 +443,9 @@ class Client:
|
|||||||
with open(self.arm_template, "r") as template_handle:
|
with open(self.arm_template, "r") as template_handle:
|
||||||
template = json.load(template_handle)
|
template = json.load(template_handle)
|
||||||
|
|
||||||
client = get_client_from_cli_profile(ResourceManagementClient)
|
client = get_client_from_cli_profile(
|
||||||
|
ResourceManagementClient, subscription_id=self.get_subscription_id()
|
||||||
|
)
|
||||||
client.resource_groups.create_or_update(
|
client.resource_groups.create_or_update(
|
||||||
self.resource_group, {"location": self.location}
|
self.resource_group, {"location": self.location}
|
||||||
)
|
)
|
||||||
@ -512,6 +528,7 @@ class Client:
|
|||||||
assign_scaleset_role(
|
assign_scaleset_role(
|
||||||
self.application_name,
|
self.application_name,
|
||||||
self.results["deploy"]["scaleset-identity"]["value"],
|
self.results["deploy"]["scaleset-identity"]["value"],
|
||||||
|
self.get_subscription_id(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def apply_migrations(self) -> None:
|
def apply_migrations(self) -> None:
|
||||||
@ -548,7 +565,9 @@ class Client:
|
|||||||
logger.info("creating eventgrid subscription")
|
logger.info("creating eventgrid subscription")
|
||||||
src_resource_id = self.results["deploy"]["fuzz-storage"]["value"]
|
src_resource_id = self.results["deploy"]["fuzz-storage"]["value"]
|
||||||
dst_resource_id = self.results["deploy"]["func-storage"]["value"]
|
dst_resource_id = self.results["deploy"]["func-storage"]["value"]
|
||||||
client = get_client_from_cli_profile(StorageManagementClient)
|
client = get_client_from_cli_profile(
|
||||||
|
StorageManagementClient, subscription_id=self.get_subscription_id()
|
||||||
|
)
|
||||||
event_subscription_info = EventSubscription(
|
event_subscription_info = EventSubscription(
|
||||||
destination=StorageQueueEventSubscriptionDestination(
|
destination=StorageQueueEventSubscriptionDestination(
|
||||||
resource_id=dst_resource_id, queue_name="file-changes"
|
resource_id=dst_resource_id, queue_name="file-changes"
|
||||||
@ -565,7 +584,9 @@ class Client:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
client = get_client_from_cli_profile(EventGridManagementClient)
|
client = get_client_from_cli_profile(
|
||||||
|
EventGridManagementClient, subscription_id=self.get_subscription_id()
|
||||||
|
)
|
||||||
result = client.event_subscriptions.create_or_update(
|
result = client.event_subscriptions.create_or_update(
|
||||||
src_resource_id, "onefuzz1", event_subscription_info
|
src_resource_id, "onefuzz1", event_subscription_info
|
||||||
).result()
|
).result()
|
||||||
@ -639,7 +660,8 @@ class Client:
|
|||||||
)
|
)
|
||||||
|
|
||||||
app_insight_client = get_client_from_cli_profile(
|
app_insight_client = get_client_from_cli_profile(
|
||||||
ApplicationInsightsManagementClient
|
ApplicationInsightsManagementClient,
|
||||||
|
subscription_id=self.get_subscription_id(),
|
||||||
)
|
)
|
||||||
|
|
||||||
to_delete = []
|
to_delete = []
|
||||||
@ -829,7 +851,7 @@ class Client:
|
|||||||
def update_registration(self) -> None:
|
def update_registration(self) -> None:
|
||||||
if not self.create_registration:
|
if not self.create_registration:
|
||||||
return
|
return
|
||||||
update_pool_registration(self.application_name)
|
update_pool_registration(self.application_name, self.get_subscription_id())
|
||||||
|
|
||||||
def done(self) -> None:
|
def done(self) -> None:
|
||||||
logger.info(TELEMETRY_NOTICE)
|
logger.info(TELEMETRY_NOTICE)
|
||||||
@ -967,6 +989,10 @@ def main() -> None:
|
|||||||
default=None,
|
default=None,
|
||||||
help="enable multi-tenant authentication with this tenant domain",
|
help="enable multi-tenant authentication with this tenant domain",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--subscription_id",
|
||||||
|
type=str,
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if shutil.which("func") is None:
|
if shutil.which("func") is None:
|
||||||
@ -992,6 +1018,7 @@ def main() -> None:
|
|||||||
log_service_principal=args.log_service_principal,
|
log_service_principal=args.log_service_principal,
|
||||||
multi_tenant_domain=args.multi_tenant_domain,
|
multi_tenant_domain=args.multi_tenant_domain,
|
||||||
upgrade=args.upgrade,
|
upgrade=args.upgrade,
|
||||||
|
subscription_id=args.subscription_id,
|
||||||
)
|
)
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
level = logging.DEBUG
|
level = logging.DEBUG
|
||||||
|
@ -80,9 +80,10 @@ def query_microsoft_graph(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_graph_client() -> GraphRbacManagementClient:
|
def get_graph_client(subscription_id: str) -> GraphRbacManagementClient:
|
||||||
client: GraphRbacManagementClient = get_client_from_cli_profile(
|
client: GraphRbacManagementClient = get_client_from_cli_profile(
|
||||||
GraphRbacManagementClient
|
GraphRbacManagementClient,
|
||||||
|
subscription_id=subscription_id,
|
||||||
)
|
)
|
||||||
return client
|
return client
|
||||||
|
|
||||||
@ -99,10 +100,13 @@ class OnefuzzAppRole(Enum):
|
|||||||
|
|
||||||
|
|
||||||
def register_application(
|
def register_application(
|
||||||
registration_name: str, onefuzz_instance_name: str, approle: OnefuzzAppRole
|
registration_name: str,
|
||||||
|
onefuzz_instance_name: str,
|
||||||
|
approle: OnefuzzAppRole,
|
||||||
|
subscription_id: str,
|
||||||
) -> ApplicationInfo:
|
) -> ApplicationInfo:
|
||||||
logger.info("retrieving the application registration %s" % registration_name)
|
logger.info("retrieving the application registration %s" % registration_name)
|
||||||
client = get_graph_client()
|
client = get_graph_client(subscription_id)
|
||||||
apps: List[Application] = list(
|
apps: List[Application] = list(
|
||||||
client.applications.list(filter="displayName eq '%s'" % registration_name)
|
client.applications.list(filter="displayName eq '%s'" % registration_name)
|
||||||
)
|
)
|
||||||
@ -110,7 +114,7 @@ def register_application(
|
|||||||
if len(apps) == 0:
|
if len(apps) == 0:
|
||||||
logger.info("No existing registration found. creating a new one")
|
logger.info("No existing registration found. creating a new one")
|
||||||
app = create_application_registration(
|
app = create_application_registration(
|
||||||
onefuzz_instance_name, registration_name, approle
|
onefuzz_instance_name, registration_name, approle, subscription_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
app = apps[0]
|
app = apps[0]
|
||||||
@ -136,7 +140,7 @@ def register_application(
|
|||||||
if app.app_id not in [app.app_id for app in pre_authorized_applications]:
|
if app.app_id not in [app.app_id for app in pre_authorized_applications]:
|
||||||
authorize_application(UUID(app.app_id), UUID(onefuzz_app.app_id))
|
authorize_application(UUID(app.app_id), UUID(onefuzz_app.app_id))
|
||||||
|
|
||||||
password = create_application_credential(registration_name)
|
password = create_application_credential(registration_name, subscription_id)
|
||||||
|
|
||||||
return ApplicationInfo(
|
return ApplicationInfo(
|
||||||
client_id=app.app_id,
|
client_id=app.app_id,
|
||||||
@ -145,27 +149,27 @@ def register_application(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_application_credential(application_name: str) -> str:
|
def create_application_credential(application_name: str, subscription_id: str) -> str:
|
||||||
""" Add a new password to the application registration """
|
""" Add a new password to the application registration """
|
||||||
|
|
||||||
logger.info("creating application credential for '%s'" % application_name)
|
logger.info("creating application credential for '%s'" % application_name)
|
||||||
client = get_graph_client()
|
client = get_graph_client(subscription_id)
|
||||||
apps: List[Application] = list(
|
apps: List[Application] = list(
|
||||||
client.applications.list(filter="displayName eq '%s'" % application_name)
|
client.applications.list(filter="displayName eq '%s'" % application_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
app: Application = apps[0]
|
app: Application = apps[0]
|
||||||
|
|
||||||
(_, password) = add_application_password(app.object_id)
|
(_, password) = add_application_password(app.object_id, subscription_id)
|
||||||
return str(password)
|
return str(password)
|
||||||
|
|
||||||
|
|
||||||
def create_application_registration(
|
def create_application_registration(
|
||||||
onefuzz_instance_name: str, name: str, approle: OnefuzzAppRole
|
onefuzz_instance_name: str, name: str, approle: OnefuzzAppRole, subscription_id: str
|
||||||
) -> Application:
|
) -> Application:
|
||||||
""" Create an application registration """
|
""" Create an application registration """
|
||||||
|
|
||||||
client = get_graph_client()
|
client = get_graph_client(subscription_id)
|
||||||
apps: List[Application] = list(
|
apps: List[Application] = list(
|
||||||
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
|
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
|
||||||
)
|
)
|
||||||
@ -207,7 +211,6 @@ def create_application_registration(
|
|||||||
try:
|
try:
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
client = get_graph_client()
|
|
||||||
update_param = ApplicationUpdateParameters(
|
update_param = ApplicationUpdateParameters(
|
||||||
reply_urls=["https://%s.azurewebsites.net" % onefuzz_instance_name]
|
reply_urls=["https://%s.azurewebsites.net" % onefuzz_instance_name]
|
||||||
)
|
)
|
||||||
@ -221,7 +224,9 @@ def create_application_registration(
|
|||||||
return registered_app
|
return registered_app
|
||||||
|
|
||||||
|
|
||||||
def add_application_password(app_object_id: UUID) -> Tuple[str, str]:
|
def add_application_password(
|
||||||
|
app_object_id: UUID, subscription_id: str
|
||||||
|
) -> Tuple[str, str]:
|
||||||
# Work-around the race condition where the app is created but passwords cannot
|
# Work-around the race condition where the app is created but passwords cannot
|
||||||
# be created yet.
|
# be created yet.
|
||||||
|
|
||||||
@ -234,7 +239,7 @@ def add_application_password(app_object_id: UUID) -> Tuple[str, str]:
|
|||||||
if count > 1:
|
if count > 1:
|
||||||
logging.info("retrying app password creation")
|
logging.info("retrying app password creation")
|
||||||
try:
|
try:
|
||||||
password = add_application_password_impl(app_object_id)
|
password = add_application_password_impl(app_object_id, subscription_id)
|
||||||
logging.info("app password created")
|
logging.info("app password created")
|
||||||
return password
|
return password
|
||||||
except GraphQueryError as err:
|
except GraphQueryError as err:
|
||||||
@ -255,11 +260,13 @@ def add_application_password(app_object_id: UUID) -> Tuple[str, str]:
|
|||||||
raise Exception("unable to create password")
|
raise Exception("unable to create password")
|
||||||
|
|
||||||
|
|
||||||
def add_application_password_legacy(app_object_id: UUID) -> Tuple[str, str]:
|
def add_application_password_legacy(
|
||||||
|
app_object_id: UUID, subscription_id: str
|
||||||
|
) -> Tuple[str, str]:
|
||||||
key = str(uuid4())
|
key = str(uuid4())
|
||||||
password = str(uuid4())
|
password = str(uuid4())
|
||||||
|
|
||||||
client = get_graph_client()
|
client = get_graph_client(subscription_id)
|
||||||
password_cred = [
|
password_cred = [
|
||||||
PasswordCredential(
|
PasswordCredential(
|
||||||
start_date="%s" % datetime.now(TZ_UTC).strftime("%Y-%m-%dT%H:%M.%fZ"),
|
start_date="%s" % datetime.now(TZ_UTC).strftime("%Y-%m-%dT%H:%M.%fZ"),
|
||||||
@ -275,7 +282,9 @@ def add_application_password_legacy(app_object_id: UUID) -> Tuple[str, str]:
|
|||||||
return (key, password)
|
return (key, password)
|
||||||
|
|
||||||
|
|
||||||
def add_application_password_impl(app_object_id: UUID) -> Tuple[str, str]:
|
def add_application_password_impl(
|
||||||
|
app_object_id: UUID, subscription_id: str
|
||||||
|
) -> Tuple[str, str]:
|
||||||
key = uuid4()
|
key = uuid4()
|
||||||
password_request = {
|
password_request = {
|
||||||
"passwordCredential": {
|
"passwordCredential": {
|
||||||
@ -296,7 +305,7 @@ def add_application_password_impl(app_object_id: UUID) -> Tuple[str, str]:
|
|||||||
)
|
)
|
||||||
return (str(key), password["secretText"])
|
return (str(key), password["secretText"])
|
||||||
except adal.AdalError:
|
except adal.AdalError:
|
||||||
return add_application_password_legacy(app_object_id)
|
return add_application_password_legacy(app_object_id, subscription_id)
|
||||||
|
|
||||||
|
|
||||||
def get_application(app_id: UUID) -> Optional[Any]:
|
def get_application(app_id: UUID) -> Optional[Any]:
|
||||||
@ -359,13 +368,17 @@ def authorize_application(
|
|||||||
|
|
||||||
|
|
||||||
def create_and_display_registration(
|
def create_and_display_registration(
|
||||||
onefuzz_instance_name: str, registration_name: str, approle: OnefuzzAppRole
|
onefuzz_instance_name: str,
|
||||||
|
registration_name: str,
|
||||||
|
approle: OnefuzzAppRole,
|
||||||
|
subscription_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
logger.info("Updating application registration")
|
logger.info("Updating application registration")
|
||||||
application_info = register_application(
|
application_info = register_application(
|
||||||
registration_name=registration_name,
|
registration_name=registration_name,
|
||||||
onefuzz_instance_name=onefuzz_instance_name,
|
onefuzz_instance_name=onefuzz_instance_name,
|
||||||
approle=approle,
|
approle=approle,
|
||||||
|
subscription_id=subscription_id,
|
||||||
)
|
)
|
||||||
logger.info("Registration complete")
|
logger.info("Registration complete")
|
||||||
logger.info("These generated credentials are valid for a year")
|
logger.info("These generated credentials are valid for a year")
|
||||||
@ -373,19 +386,20 @@ def create_and_display_registration(
|
|||||||
logger.info("client_secret: %s" % application_info.client_secret)
|
logger.info("client_secret: %s" % application_info.client_secret)
|
||||||
|
|
||||||
|
|
||||||
def update_pool_registration(onefuzz_instance_name: str) -> None:
|
def update_pool_registration(onefuzz_instance_name: str, subscription_id: str) -> None:
|
||||||
create_and_display_registration(
|
create_and_display_registration(
|
||||||
onefuzz_instance_name,
|
onefuzz_instance_name,
|
||||||
"%s_pool" % onefuzz_instance_name,
|
"%s_pool" % onefuzz_instance_name,
|
||||||
OnefuzzAppRole.ManagedNode,
|
OnefuzzAppRole.ManagedNode,
|
||||||
|
subscription_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def assign_scaleset_role_manually(
|
def assign_scaleset_role_manually(
|
||||||
onefuzz_instance_name: str, scaleset_name: str
|
onefuzz_instance_name: str, scaleset_name: str, subscription_id: str
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
client = get_graph_client()
|
client = get_graph_client(subscription_id)
|
||||||
apps: List[Application] = list(
|
apps: List[Application] = list(
|
||||||
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
|
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
|
||||||
)
|
)
|
||||||
@ -441,7 +455,9 @@ def assign_scaleset_role_manually(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def assign_scaleset_role(onefuzz_instance_name: str, scaleset_name: str) -> None:
|
def assign_scaleset_role(
|
||||||
|
onefuzz_instance_name: str, scaleset_name: str, subscription_id: str
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Allows the nodes in the scaleset to access the service by assigning
|
Allows the nodes in the scaleset to access the service by assigning
|
||||||
their managed identity to the ManagedNode Role
|
their managed identity to the ManagedNode Role
|
||||||
@ -513,7 +529,9 @@ def assign_scaleset_role(onefuzz_instance_name: str, scaleset_name: str) -> None
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except adal.AdalError:
|
except adal.AdalError:
|
||||||
assign_scaleset_role_manually(onefuzz_instance_name, scaleset_name)
|
assign_scaleset_role_manually(
|
||||||
|
onefuzz_instance_name, scaleset_name, subscription_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_app_audience(objectId: str, audience: str) -> None:
|
def set_app_audience(objectId: str, audience: str) -> None:
|
||||||
@ -555,6 +573,7 @@ def main() -> None:
|
|||||||
parent_parser.add_argument(
|
parent_parser.add_argument(
|
||||||
"onefuzz_instance", help="the name of the onefuzz instance"
|
"onefuzz_instance", help="the name of the onefuzz instance"
|
||||||
)
|
)
|
||||||
|
parent_parser.add_argument("subscription_id")
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
formatter_class=formatter,
|
formatter_class=formatter,
|
||||||
@ -593,14 +612,19 @@ def main() -> None:
|
|||||||
|
|
||||||
onefuzz_instance_name = args.onefuzz_instance
|
onefuzz_instance_name = args.onefuzz_instance
|
||||||
if args.command == "update_pool_registration":
|
if args.command == "update_pool_registration":
|
||||||
update_pool_registration(onefuzz_instance_name)
|
update_pool_registration(onefuzz_instance_name, args.subscription_id)
|
||||||
elif args.command == "create_cli_registration":
|
elif args.command == "create_cli_registration":
|
||||||
registration_name = args.registration_name or ("%s_cli" % onefuzz_instance_name)
|
registration_name = args.registration_name or ("%s_cli" % onefuzz_instance_name)
|
||||||
create_and_display_registration(
|
create_and_display_registration(
|
||||||
onefuzz_instance_name, registration_name, OnefuzzAppRole.CliClient
|
onefuzz_instance_name,
|
||||||
|
registration_name,
|
||||||
|
OnefuzzAppRole.CliClient,
|
||||||
|
args.subscription_id,
|
||||||
)
|
)
|
||||||
elif args.command == "assign_scaleset_role":
|
elif args.command == "assign_scaleset_role":
|
||||||
assign_scaleset_role(onefuzz_instance_name, args.scaleset_name)
|
assign_scaleset_role(
|
||||||
|
onefuzz_instance_name, args.scaleset_name, args.subscription_id
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception("invalid arguments")
|
raise Exception("invalid arguments")
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user