Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
- [Azure Funcions] Updated default funcions plan to Flex Consumption
- [AWS EC2] Updated default Ubuntu Image to Ubuntu 24
- [Azure VMS] Updated default Ubuntu Image to Ubuntu 24
- [IBM VPC] Updated default Ubuntu image to 24.04
- [IBM VPC] Changed default SSH user to `ubuntu`
- [IBM VPC] Updated `lithops image list` to show Ubuntu 22 and 24 images
- [Aliyun FC] Updated backend to Function Compute 3.0 (FC3 API) and added custom-container deploy mode support
- [Code Engine] Rewrote backend to use the IBM Code Engine SDK v2 (`CodeEngineV2`) instead of the Kubernetes API

Expand All @@ -30,6 +33,10 @@
- [Azure] Fixed Azure Functions deployment on Flex Consumption and consolidated container registry login across Azure backends
- [Oracle Object Storage] Fixed authentication with `~` in `key_file`, resource principal fallback, and bucket name generation
- [Oracle Functions] Fixed Python 3.12 runtime build, added OCIR registry login with auto-derived `docker_user`, and default `docker_server` from region
- [Standalone] Fixed SSH key permissions for the `ubuntu` user on master VMs
- [Standalone] Fixed pyOpenSSL/cryptography conflict on Ubuntu 24.04 for `ibm_cos`
- [IBM VPC] Fixed `home_dir` when using non-root SSH users
- [IBM VPC] Fixed default image selection to use Ubuntu 24 stock images only


## [v3.6.4]
Expand Down
99 changes: 86 additions & 13 deletions lithops/serverless/backends/code_engine/code_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,48 @@ def _create_code_engine_client(self):
self.ce_client = CodeEngineV2(authenticator=authenticator)
self.ce_client.set_service_url(config.BASE_URL_V2.format(self.region))

def _get_or_create_namespace(self, create=True):
def _wait_for_project_active(self, project_id=None):
"""
Waits until a Code Engine project becomes active
"""
project_id = project_id or self.project_id
if not project_id:
return

self._create_code_engine_client()
logger.debug(f"Waiting for Code Engine project {self.project_name} to become active")
deadline = time.time() + config.PROJECT_READY_TIMEOUT
while time.time() < deadline:
try:
project = self.ce_client.get_project(id=project_id).get_result()
except ApiException as e:
not_ready = (
e.status_code in (403, 404, 503)
or 'not yet active' in (e.message or '').lower()
)
if not_ready:
logger.debug(
f"Code Engine project {self.project_name} status: creating"
)
time.sleep(config.PROJECT_POLL_INTERVAL)
continue
raise e

status = project.get('status')
if status == 'active':
logger.debug(f"Code Engine project {self.project_name} is active")
return
if status == 'creation_failed':
raise Exception(f"Code Engine project {self.project_name} creation failed")

logger.debug(f"Code Engine project {self.project_name} status: {status}")
time.sleep(config.PROJECT_POLL_INTERVAL)

raise Exception(
f"Timed out waiting for Code Engine project {self.project_name} to become active"
)

def _get_or_create_namespace(self, create=True, wait_for_active=True):
"""
Gets or creates the Code Engine project.
Namespace is kept for runtime key compatibility.
Expand All @@ -126,8 +167,26 @@ def _get_or_create_namespace(self, create=True):
self.namespace = ce_data.get('namespace')

if self.project_id:
self._sync_project_config()
return self.namespace
self._create_code_engine_client()
try:
project = self.ce_client.get_project(id=self.project_id).get_result()
status = project.get('status')
if status == 'active':
self._sync_project_config()
return self.namespace
if status in ('creating', 'preparing') and wait_for_active:
self._wait_for_project_active()
self._sync_project_config()
return self.namespace
if status in ('creating', 'preparing'):
self._sync_project_config()
return self.namespace
except ApiException as e:
if e.status_code not in (403, 404):
raise e
logger.debug(f"Cached project {self.project_id} is unavailable, refreshing")
self.project_id = None
self.namespace = None

self._create_code_engine_client()

Expand All @@ -136,15 +195,28 @@ def _get_or_create_namespace(self, create=True):
if project['name'] == self.project_name:
logger.debug(f"Found Code Engine project: {self.project_name}")
self.project_id = project['id']
if project.get('status') != 'active' and wait_for_active:
self._wait_for_project_active()
break

if not self.project_id and create:
logger.debug(f"Creating new Code Engine project: {self.project_name}")
response = self.ce_client.create_project(
name=self.project_name,
resource_group_id=self.config['resource_group_id']
).get_result()
try:
response = self.ce_client.create_project(
name=self.project_name,
resource_group_id=self.config['resource_group_id']
).get_result()
except ApiException as e:
if e.status_code == 409 and 'soft-deleted' in (e.message or '').lower():
raise Exception(
f"The Code Engine project '{self.project_name}' is soft-deleted "
f"and kept for up to 7 days before it is permanently removed. "
f"To delete it completely and reuse the name now, run:\n"
f"ibmcloud ce project delete --name {self.project_name} --hard -f"
) from e
raise e
self.project_id = response['id']
self._wait_for_project_active()

if not self.project_id:
return None
Expand Down Expand Up @@ -667,7 +739,7 @@ def delete_runtime(self, runtime_name, memory, version=__version__, jobdef_name=
Deletes a runtime.
We need to delete the job definition.
"""
if not self._get_or_create_namespace(create=False):
if not self._get_or_create_namespace(create=False, wait_for_active=False):
logger.info(f"Project {self.project_name} does not exist")
return

Expand All @@ -682,7 +754,7 @@ def clean(self, all=False):
Deletes all runtimes from all packages
"""
logger.info(f'Cleaning project {self.project_name}')
if not self._get_or_create_namespace(create=False):
if not self._get_or_create_namespace(create=False, wait_for_active=False):
logger.info(f"Project {self.project_name} does not exist")
if os.path.exists(self.cache_file):
os.remove(self.cache_file)
Expand All @@ -694,17 +766,18 @@ def clean(self, all=False):

self._delete_lithops_config_maps()

if all and os.path.exists(self.cache_file):
if all and self.project_id:
logger.info(f"Deleting Code Engine project: {self.project_name}")
self.ce_client.delete_project(id=self.project_id)
os.remove(self.cache_file)
if os.path.exists(self.cache_file):
os.remove(self.cache_file)

def list_runtimes(self, docker_image_name='all'):
"""
List all the runtimes
return: list of tuples (docker_image_name, memory, version, jobdef_name)
"""
if not self._get_or_create_namespace(create=False):
if not self._get_or_create_namespace(create=False, wait_for_active=False):
logger.info(f"Project {self.project_name} does not exist")
return []

Expand All @@ -714,7 +787,7 @@ def clear(self, job_keys=None):
"""
Clean all completed jobruns in the current executor
"""
if not self._get_or_create_namespace(create=False):
if not self._get_or_create_namespace(create=False, wait_for_active=False):
logger.info(f"Project {self.project_name} does not exist")
return

Expand Down
2 changes: 2 additions & 0 deletions lithops/serverless/backends/code_engine/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
PYTHON_BIN = '/usr/local/bin/python'
METADATA_JOBRUN_NAME = 'lithops-runtime-metadata'
JOB_RUN_POLL_INTERVAL = 2
PROJECT_POLL_INTERVAL = 10
PROJECT_READY_TIMEOUT = 600

# https://cloud.ibm.com/docs/codeengine?topic=codeengine-mem-cpu-combo
VALID_CPU_VALUES = [0.125, 0.25, 0.5, 1, 2, 4, 6, 8]
Expand Down
11 changes: 10 additions & 1 deletion lithops/standalone/backends/ibm_vpc/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
'master_profile_name': 'cx2-2x4',
'worker_profile_name': 'cx2-2x4',
'boot_volume_profile': 'general-purpose',
'ssh_username': 'root',
'ssh_username': 'ubuntu',
'ssh_password': str(uuid.uuid4()),
'ssh_key_filename': '~/.ssh/id_rsa',
'delete_on_dismantle': True,
Expand All @@ -45,6 +45,15 @@

REGIONS = ["jp-tok", "jp-osa", "au-syd", "eu-gb", "eu-de", "eu-es", "us-south", "us-east", "br-sao", "ca-tor"]

INSTANCE_START_TIMEOUT = 180
VPC_API_VERSION = '2021-09-21'

DEFAULT_LITHOPS_IMAGE_NAME = 'lithops-ubuntu-24-04-4-minimal-amd64-1'
DEFAULT_UBUNTU_LTS_MAJOR = 24
DEFAULT_STOCK_UBUNTU_IMAGE_PREFIX = f'ibm-ubuntu-{DEFAULT_UBUNTU_LTS_MAJOR}'
# Ubuntu LTS major versions shown by `lithops image list` (inclusive range).
LIST_UBUNTU_LTS_RANGE = (22, DEFAULT_UBUNTU_LTS_MAJOR)


def load_config(config_data):

Expand Down
99 changes: 77 additions & 22 deletions lithops/standalone/backends/ibm_vpc/ibm_vpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@
prepare_standalone_clean,
standalone_clean_stop_early,
)
from .config import (
DEFAULT_LITHOPS_IMAGE_NAME,
DEFAULT_STOCK_UBUNTU_IMAGE_PREFIX,
DEFAULT_UBUNTU_LTS_MAJOR,
INSTANCE_START_TIMEOUT,
LIST_UBUNTU_LTS_RANGE,
VPC_API_VERSION,
)

logger = logging.getLogger(__name__)

INSTANCE_START_TIMEOUT = 180
VPC_API_VERSION = '2021-09-21'

DEFAULT_LITHOPS_IMAGE_NAME = 'lithops-ubuntu-22-04-3-minimal-amd64-1'


class IBMVPCBackend:

Expand Down Expand Up @@ -504,8 +507,8 @@ def _request_image_id(self):

if 'image_id' in self.vpc_data:
for image in images_def:
if image['id'] == self.vpc_data['image_id'] and \
not image['name'].startswith('ibm-ubuntu-22'):
if image['id'] == self.vpc_data['image_id'] \
and self._is_deploy_ubuntu_image(image):
self.config['image_id'] = self.vpc_data['image_id']
break

Expand All @@ -517,11 +520,15 @@ def _request_image_id(self):
break

if 'image_id' not in self.config:
for image in images_def:
if image['name'].startswith('ibm-ubuntu-22') \
and "amd64" in image['name']:
self.config['image_id'] = image['id']
break
stock_images = [
image for image in images_def
if image['name'].startswith(DEFAULT_STOCK_UBUNTU_IMAGE_PREFIX)
and 'amd64' in image['name']
]
if stock_images:
stock_images.sort(key=lambda image: image['name'], reverse=True)
self.config['image_id'] = stock_images[0]['id']
logger.debug(f"Using stock VM image: {stock_images[0]['name']}")

def _create_master_instance(self):
"""
Expand Down Expand Up @@ -728,9 +735,56 @@ def list_images():
time.sleep(2)
logger.debug(f"VM Image '{image_name}' successfully deleted")

@staticmethod
def _ubuntu_lts_major_version(image_name, display_name=None):
for text in (display_name or '', image_name):
match = re.search(r'ubuntu[- ](\d{2})', text.lower())
if match:
return int(match.group(1))
match = re.search(r'(\d{2})\.\d{2}', text)
if match:
return int(match.group(1))
return None

@classmethod
def _is_ubuntu_amd64_image(cls, image):
return (
image['operating_system']['family'] == 'Ubuntu Linux'
and 'amd64' in image['name']
)

@classmethod
def _is_supported_ubuntu_image(cls, image, lts_range=None):
if not cls._is_ubuntu_amd64_image(image):
return False
if 'lithops' in image['name'].lower():
return True
major = cls._ubuntu_lts_major_version(
image['name'],
image['operating_system'].get('display_name'),
)
if major is None:
return False
if lts_range is None:
return major == DEFAULT_UBUNTU_LTS_MAJOR
min_major, max_major = lts_range
return min_major <= major <= max_major

@classmethod
def _is_deploy_ubuntu_image(cls, image):
"""
Images valid for provisioning master/worker VSIs (Ubuntu 24 stock or Lithops custom).
"""
if not cls._is_ubuntu_amd64_image(image):
return False
if 'lithops' in image['name'].lower():
return cls._is_supported_ubuntu_image(image)
return image['name'].startswith(DEFAULT_STOCK_UBUNTU_IMAGE_PREFIX)

def list_images(self):
"""
List VM Images
List Ubuntu LTS images (22.04–24.04) and Lithops custom images.
Returns tuples of (name, image_id, creation_date).
"""
images_def = self.vpc_cli.list_images().result['images']
images_user = self.vpc_cli.list_images(resource_group_id=self.config['resource_group_id']).result['images']
Expand All @@ -739,14 +793,14 @@ def list_images(self):
result = set()

for img in images_def:
if img['operating_system']['family'] == 'Ubuntu Linux':
opsys = img['operating_system']['display_name']
image_name = img['name']
image_id = img['id']
created_at = datetime.strptime(img['created_at'], "%Y-%m-%dT%H:%M:%SZ")
created_at = created_at.strftime("%Y-%m-%d %H:%M:%S")
if '22' in opsys:
result.add((image_name, image_id, created_at))
if not self._is_supported_ubuntu_image(img, lts_range=LIST_UBUNTU_LTS_RANGE):
continue

image_name = img['name']
image_id = img['id']
created_at = datetime.strptime(img['created_at'], "%Y-%m-%dT%H:%M:%SZ")
created_at = created_at.strftime("%Y-%m-%d %H:%M:%S")
result.add((image_name, image_id, created_at))

return sorted(result, key=lambda x: x[2], reverse=True)

Expand Down Expand Up @@ -1011,7 +1065,8 @@ def __init__(self, name, ibm_vpc_config, ibm_vpc_client=None, public=False):
self.instance_data = None
self.private_ip = None
self.public_ip = None
self.home_dir = '/root'
ssh_user = self.config['ssh_username']
self.home_dir = '/root' if ssh_user == 'root' else f'/home/{ssh_user}'

self.ssh_credentials = {
'username': self.config['ssh_username'],
Expand Down
15 changes: 13 additions & 2 deletions lithops/standalone/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ def get_host_setup_script(
# --ignore-installed: do not uninstall Debian python packages (avoids RECORD errors)
pip3 install --ignore-installed -U pip
pip3 install --ignore-installed flask gevent {lithops_pip_spec}
if echo "{lithops_pip_spec}" | grep -q ibm; then
echo "--> Upgrading pyOpenSSL/cryptography (required for ibm_cos on Ubuntu 24.04)"
pip3 install --ignore-installed --upgrade 'pyopenssl>=24.0.0' 'cryptography>=42.0.0'
fi;
fi;

EXTRA_PY="{extra_python_packages}"
Expand Down Expand Up @@ -350,11 +354,18 @@ def get_master_setup_script(config, vm_data):
generate_ssh_key(){{
echo ' StrictHostKeyChecking no
UserKnownHostsFile=/dev/null' >> /etc/ssh/ssh_config;
mkdir -p $USER_HOME/.ssh;
chmod 700 $USER_HOME/.ssh;
chown ${{SUDO_USER}}:${{SUDO_USER}} $USER_HOME/.ssh;
ssh-keygen -f $USER_HOME/.ssh/lithops_id_rsa -t rsa -N '';
chown ${{SUDO_USER}}:${{SUDO_USER}} $USER_HOME/.ssh/lithops_id_rsa*;
cp $USER_HOME/.ssh/lithops_id_rsa $USER_HOME/.ssh/id_rsa
cp $USER_HOME/.ssh/lithops_id_rsa.pub $USER_HOME/.ssh/id_rsa.pub
cp $USER_HOME/.ssh/* /root/.ssh;
chown ${{SUDO_USER}}:${{SUDO_USER}} $USER_HOME/.ssh/lithops_id_rsa* $USER_HOME/.ssh/id_rsa $USER_HOME/.ssh/id_rsa.pub
chmod 600 $USER_HOME/.ssh/lithops_id_rsa $USER_HOME/.ssh/id_rsa
chmod 644 $USER_HOME/.ssh/lithops_id_rsa.pub $USER_HOME/.ssh/id_rsa.pub
cp $USER_HOME/.ssh/lithops_id_rsa /root/.ssh/lithops_id_rsa
cp $USER_HOME/.ssh/lithops_id_rsa.pub /root/.ssh/lithops_id_rsa.pub
chmod 600 /root/.ssh/lithops_id_rsa
echo '127.0.0.1 lithops-master' >> /etc/hosts;
cat $USER_HOME/.ssh/id_rsa.pub >> $USER_HOME/.ssh/authorized_keys;
}}
Expand Down
Loading
Loading