]> git.ipfire.org Git - thirdparty/ipxe.git/commitdiff
[cloud] Support creation of a censorship bypass role for Alibaba Cloud master
authorMichael Brown <mcb30@ipxe.org>
Tue, 14 Apr 2026 12:59:38 +0000 (13:59 +0100)
committerMichael Brown <mcb30@ipxe.org>
Tue, 14 Apr 2026 13:22:21 +0000 (14:22 +0100)
Importing images into Alibaba Cloud currently relies upon using a
temporary Function Compute function to work around Chinese state
censorship laws that prevent direct access to OSS bucket contents in
mainland China regions.

Unfortunately, Alibaba Cloud regions are extremely asymmetric in terms
of feature support.  (For example, some regions do not even support
IPv6 networking.)  Several mainland China regions do not support
Function Compute, and so this workaround is not available for those
regions.

A possible alternative censorship workaround is to create temporary
ECS virtual machine instances instead of temporary Function Compute
functions.  This requires the existence of a role that can be used by
ECS instances to access OSS.  We cannot use the AliyunFcDefaultRole
that is currently used by Function Compute, since this role cannot be
assumed by ECS instances.

Creating roles is a privileged operation, and it would be sensible to
assume that the image importer (which may be running as part of a
GitHub Actions workflow) may not have permission to itself create a
suitable temporary role.  The censorship bypass role must therefore be
set up once in advance by a suitably privileged user.

Add the ability to create a suitable censorship bypass role to the
Alibaba Cloud setup utility.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
contrib/cloud/ali-setup

index 4a182e803344db781d3514d56725f669234989aa..88d7ab296b1f39183ec1a73dd81bb01d9e1ae781 100755 (executable)
@@ -5,6 +5,7 @@ from collections import namedtuple
 from concurrent.futures import ThreadPoolExecutor, as_completed
 import ipaddress
 from itertools import islice
 from concurrent.futures import ThreadPoolExecutor, as_completed
 import ipaddress
 from itertools import islice
+import json
 import time
 
 import alibabacloud_credentials as credentials
 import time
 
 import alibabacloud_credentials as credentials
@@ -13,6 +14,9 @@ import alibabacloud_credentials.models
 import alibabacloud_ecs20140526 as ecs
 import alibabacloud_ecs20140526.client
 import alibabacloud_ecs20140526.models
 import alibabacloud_ecs20140526 as ecs
 import alibabacloud_ecs20140526.client
 import alibabacloud_ecs20140526.models
+import alibabacloud_ram20150501 as ram
+import alibabacloud_ram20150501.client
+import alibabacloud_ram20150501.models
 import alibabacloud_tea_openapi as openapi
 import alibabacloud_tea_openapi.client
 import alibabacloud_tea_openapi.models
 import alibabacloud_tea_openapi as openapi
 import alibabacloud_tea_openapi.client
 import alibabacloud_tea_openapi.models
@@ -24,11 +28,22 @@ import alibabacloud_vpc20160428.client
 import alibabacloud_vpc20160428.models
 
 ECS_ENDPOINT = 'ecs.aliyuncs.com'
 import alibabacloud_vpc20160428.models
 
 ECS_ENDPOINT = 'ecs.aliyuncs.com'
+RAM_ENDPOINT = 'ram.aliyuncs.com'
 
 IPXE_VPC_TAG = 'ipxe-default-vpc'
 IPXE_VSWITCH_TAG = 'ipxe-default-vswitch'
 IPXE_SG_TAG = 'ipxe-default-sg'
 
 
 IPXE_VPC_TAG = 'ipxe-default-vpc'
 IPXE_VSWITCH_TAG = 'ipxe-default-vswitch'
 IPXE_SG_TAG = 'ipxe-default-sg'
 
+IPXE_CENSORSHIP_BYPASS_ROLE_NAME = 'iPXECensorshipBypassRole'
+IPXE_CENSORSHIP_BYPASS_ROLE_ASSUME_POLICY = {
+    'Statement': [{
+        'Action': 'sts:AssumeRole',
+        'Effect': 'Allow',
+        'Principal': {'Service': ['ecs.aliyuncs.com']},
+    }],
+    'Version': '1',
+}
+
 Clients = namedtuple('Clients', ['region', 'ecs', 'vpc'])
 
 def all_regions():
 Clients = namedtuple('Clients', ['region', 'ecs', 'vpc'])
 
 def all_regions():
@@ -52,6 +67,50 @@ def all_clients(region):
     )
     return clients
 
     )
     return clients
 
+def ram_client():
+    """Construct resource access management client"""
+    cred = credentials.client.Client()
+    conf = openapi.models.Config(credential=cred, endpoint=RAM_ENDPOINT)
+    client = ram.client.Client(conf)
+    return client
+
+def setup_censorship_bypass_role(client):
+    """Set up censorship bypass role (required for importing images)"""
+    role_name = IPXE_CENSORSHIP_BYPASS_ROLE_NAME
+    assume_policy = json.dumps(IPXE_CENSORSHIP_BYPASS_ROLE_ASSUME_POLICY)
+    req = ram.models.GetRoleRequest(
+        role_name=role_name,
+    )
+    try:
+        rsp = client.get_role(req)
+        arn = rsp.body.role.arn
+    except openapi.exceptions.ClientException as exc:
+        if exc.code != 'EntityNotExist.Role':
+            raise
+        req = ram.models.CreateRoleRequest(
+            role_name=role_name,
+            assume_role_policy_document=assume_policy,
+        )
+        rsp = client.create_role(req)
+        arn = rsp.body.role.arn
+    req = ram.models.UpdateRoleRequest(
+        role_name=role_name,
+        new_assume_role_policy_document=assume_policy,
+        new_description="iPXE role to help bypass OSS censorship restrictions",
+    )
+    rsp = client.update_role(req)
+    req = ram.models.AttachPolicyToRoleRequest(
+        role_name=role_name,
+        policy_type='System',
+        policy_name='AliyunOSSFullAccess',
+    )
+    try:
+        rsp = client.attach_policy_to_role(req)
+    except openapi.exceptions.ClientException as exc:
+        if exc.code != 'EntityAlreadyExists.Role.Policy':
+            raise
+    return arn
+
 def setup_vpc(clients):
     """Set up VPC"""
     tag = vpc.models.DescribeVpcsRequestTag(
 def setup_vpc(clients):
     """Set up VPC"""
     tag = vpc.models.DescribeVpcsRequestTag(
@@ -254,8 +313,14 @@ def setup_region(clients):
 parser = argparse.ArgumentParser(description="Set up Alibaba Cloud defaults")
 parser.add_argument('--region', '-r', action='append',
                     help="AliCloud region(s)")
 parser = argparse.ArgumentParser(description="Set up Alibaba Cloud defaults")
 parser.add_argument('--region', '-r', action='append',
                     help="AliCloud region(s)")
+parser.add_argument('--create-role', action=argparse.BooleanOptionalAction,
+                    default=True, help="Create censorship bypass role")
 args = parser.parse_args()
 
 args = parser.parse_args()
 
+# Set up censorship bypass role
+if args.create_role:
+    arn = setup_censorship_bypass_role(ram_client())
+
 # Use all regions if none specified
 if not args.region:
     args.region = all_regions()
 # Use all regions if none specified
 if not args.region:
     args.region = all_regions()
@@ -271,6 +336,8 @@ with ThreadPoolExecutor(max_workers=len(args.region)) as executor:
     results = {futures[x]: x.result() for x in as_completed(futures)}
 
 # Show created resources
     results = {futures[x]: x.result() for x in as_completed(futures)}
 
 # Show created resources
+if args.create_role:
+    print("%s" % arn)
 for region in args.region:
     (sg_id, vpc_id, vswitch_ids) = results[region]
     print("%s %s %s %s" % (region, sg_id, vpc_id, " ".join(vswitch_ids)))
 for region in args.region:
     (sg_id, vpc_id, vswitch_ids) = results[region]
     print("%s %s %s %s" % (region, sg_id, vpc_id, " ".join(vswitch_ids)))