From f70634d5a1600b74233ee50103aa97be61c803ad Mon Sep 17 00:00:00 2001 From: WangYunHua-TREC <10154231wang_yunhua@cn.tre-inc.com> Date: Wed, 14 Aug 2024 11:08:03 +0800 Subject: [PATCH 1/9] Note relationship of pipeline and model(AIGEAR-42) --- src/aigear/manage/local/db_models.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/aigear/manage/local/db_models.py b/src/aigear/manage/local/db_models.py index 63835df..8c06058 100644 --- a/src/aigear/manage/local/db_models.py +++ b/src/aigear/manage/local/db_models.py @@ -1,10 +1,11 @@ -from sqlalchemy import Column, Integer, String, DateTime, JSON +from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey +from sqlalchemy.orm import relationship from datetime import datetime from .init_db import Base class ModelMeta(Base): - __tablename__ = "model_meta" + __tablename__ = "models" id = Column(Integer, primary_key=True, autoincrement=True) author = Column(String) @@ -16,9 +17,20 @@ class ModelMeta(Base): created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + # pipeline_id = Column(Integer, ForeignKey('pipelines.id')) + # pipelines = relationship("Pipeline", back_populates="models") + +# # The relationship between the pipeline and the model +# class PipelineModels: +# __tablename__ = "pipeline_models" + +# id = Column(Integer, primary_key=True, autoincrement=True) +# pipeline_id = Column(Integer) +# model_id = Column(Integer) + class PipelineMeta(Base): - __tablename__ = "pipeline_meta" + __tablename__ = "pipelines" id = Column(Integer, primary_key=True, autoincrement=True) author = Column(String) @@ -28,3 +40,5 @@ class PipelineMeta(Base): tags = Column(JSON) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + # models = relationship("Models", back_populates="model") From 3adec153ec16da4a5f25b221d270936e2b86ae33 Mon Sep 17 00:00:00 2001 From: WangYunHua-TREC <10154231wang_yunhua@cn.tre-inc.com> Date: Thu, 15 Aug 2024 16:53:54 +0800 Subject: [PATCH 2/9] Fix some issues with aigaer(AIGEAR-43) --- example/fm/Dockerfile | 11 +++++++ example/fm/aigear-0.0.1-py3-none-any.whl | Bin 0 -> 40595 bytes example/fm/pipeline.py | 5 +-- example/fm/requirements.txt | 7 +++-- example/pipeline/Dockerfile | 2 +- example/pipeline/iris_pipeline.py | 5 ++- pyproject.toml | 2 ++ src/aigear/deploy/docker/builder.py | 24 +++++++++------ src/aigear/deploy/docker/define_dockerfile.py | 2 +- src/aigear/microservices/grpc/service.py | 2 +- src/aigear/pipeline/pipeline.py | 29 ++++++++++-------- 11 files changed, 58 insertions(+), 31 deletions(-) create mode 100644 example/fm/Dockerfile create mode 100644 example/fm/aigear-0.0.1-py3-none-any.whl diff --git a/example/fm/Dockerfile b/example/fm/Dockerfile new file mode 100644 index 0000000..2c81a61 --- /dev/null +++ b/example/fm/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.10 +WORKDIR /aigear/fm/ +COPY . /aigear/fm/ +COPY requirements.txt /aigear/fm/requirements.txt + +RUN python -m pip install --upgrade pip +RUN python -m pip install -r /aigear/fm/requirements.txt +RUN python -m pip install aigear-0.0.1-py3-none-any.whl + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONBUFFERED=1 diff --git a/example/fm/aigear-0.0.1-py3-none-any.whl b/example/fm/aigear-0.0.1-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..4040f36768d36edf7db27d3045650723b95c8e5f GIT binary patch literal 40595 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 literal 0 HcmV?d00001 diff --git a/example/fm/pipeline.py b/example/fm/pipeline.py index acf4b8b..910a018 100644 --- a/example/fm/pipeline.py +++ b/example/fm/pipeline.py @@ -369,7 +369,7 @@ def fm_pipeline(): if __name__ == '__main__': - # import os + import os # my_pipeline() fm_pipeline.run_in_executor() @@ -388,9 +388,10 @@ def fm_pipeline(): # # fm_pipeline.deploy( # volumes=volumes, - # skip_build_image=True, + # skip_build_image=False, # cpu_count=cpu_count_train, # mem_limit=mem_limit_train + # ) # ).to_service( # hostname=hostname, # ports=ports, diff --git a/example/fm/requirements.txt b/example/fm/requirements.txt index 28def66..3635f3f 100644 --- a/example/fm/requirements.txt +++ b/example/fm/requirements.txt @@ -1,3 +1,4 @@ -dask>=2024.7.0 -rankfm>=0.2.5 -scikit-learn>=1.5.0 +dask>=2022.10.1 +scikit-learn>=1.2.0 +git+https://github.com/ErraticO/rankfm.git +haversine >= 2.0.0 diff --git a/example/pipeline/Dockerfile b/example/pipeline/Dockerfile index d6a2642..ebff3e3 100644 --- a/example/pipeline/Dockerfile +++ b/example/pipeline/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 +FROM python:3.10 WORKDIR /pipeline COPY . . diff --git a/example/pipeline/iris_pipeline.py b/example/pipeline/iris_pipeline.py index ce3452c..751de20 100644 --- a/example/pipeline/iris_pipeline.py +++ b/example/pipeline/iris_pipeline.py @@ -3,6 +3,7 @@ from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score from aigear.pipeline import workflow, task +from pathlib import Path import pickle import json import os @@ -44,7 +45,9 @@ def get_env_variables(): @task def save_model(model, model_path): - with open(model_path, "wb") as md: + path = Path(model_path) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open('wb') as md: pickle.dump(model, md) diff --git a/pyproject.toml b/pyproject.toml index ea2f2f1..e667b4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,8 @@ Issues = "https://github.com/retail-ai-inc/aigear/issues" common = [ "tabulate >= 0.9", "cloudpickle >= 2.0.0", + "astor >= 0.8.1", + "sqlalchemy >= 1.1.13" ] docker = [ "docker >= 6.13", diff --git a/src/aigear/deploy/docker/builder.py b/src/aigear/deploy/docker/builder.py index 516c9bd..777ebf9 100644 --- a/src/aigear/deploy/docker/builder.py +++ b/src/aigear/deploy/docker/builder.py @@ -55,12 +55,12 @@ def push(self): @staticmethod def get_image_id(tag: str): - try: - with docker_client() as client: + with docker_client() as client: + try: image = client.images.get(tag) - return image.id - except ImageNotFound: - logger.info('Image not found.') + return image.id + except ImageNotFound: + logger.info(f'Image not found: {tag}.') def build_image( @@ -202,15 +202,21 @@ def default_dockerfile( package_source = " -i " + package_source lines.append(f"COPY requirements.txt {workdir}/requirements.txt") + lines.append( + "" + ) lines.append(f"RUN python -m pip install --upgrade pip{package_source}") lines.append( f"RUN python -m pip install -r {workdir}/requirements.txt{package_source}" ) lines.append( - f""" - ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONBUFFERED=1 - """ + "" + ) + lines.append( + "ENV PYTHONDONTWRITEBYTECODE=1" + ) + lines.append( + "ENV PYTHONBUFFERED=1" ) with Path("Dockerfile").open("w") as f: diff --git a/src/aigear/deploy/docker/define_dockerfile.py b/src/aigear/deploy/docker/define_dockerfile.py index 6a35df5..f2a90cb 100644 --- a/src/aigear/deploy/docker/define_dockerfile.py +++ b/src/aigear/deploy/docker/define_dockerfile.py @@ -15,4 +15,4 @@ with open('Dockerfile', 'w') as f: f.write(dockerfile_content) -print("Dockerfile 创建完成。") +print("Dockerfile creation completed.") diff --git a/src/aigear/microservices/grpc/service.py b/src/aigear/microservices/grpc/service.py index 796de45..784a990 100644 --- a/src/aigear/microservices/grpc/service.py +++ b/src/aigear/microservices/grpc/service.py @@ -84,7 +84,7 @@ def get_env_variables(tag: str): def check_env_variables(env_variables: dict): run_or_not = True for env_variable_name in env_variables: - if env_variables[env_variable_name] == None: + if env_variables[env_variable_name] is None: logger.error( f"{env_variable_name} not found in the env variables!") run_or_not = False diff --git a/src/aigear/pipeline/pipeline.py b/src/aigear/pipeline/pipeline.py index f0a4a5d..adc83e4 100644 --- a/src/aigear/pipeline/pipeline.py +++ b/src/aigear/pipeline/pipeline.py @@ -13,7 +13,6 @@ state, ) from ..common.callable import get_call_parameters -from ..common.hashing import file_hash, stable_hash from .executor import TaskRunner from ..deploy.docker.builder import ImageBuilder from ..deploy.docker.container import run_or_restart_container @@ -61,18 +60,22 @@ def deploy( image_id = image_builder.get_image_id(tag=self.name) else: image_id = image_builder.build(tag=self.name) - flow_path = flow_path_in_workdir(self._flow_file) - command = f"aigear-workflow --script_path {flow_path} --function_name {self.fn.__name__}" - run_or_restart_container( - container_name=self.name, - image_id=image_id, - command=command, - volumes=volumes, - ports=ports, - hostname=hostname, - is_stream_logs=is_stream_logs, - **kwargs - ) + + if image_id: + flow_path = flow_path_in_workdir(self._flow_file) + command = f"aigear-workflow --script_path {flow_path} --function_name {self.fn.__name__}" + run_or_restart_container( + container_name=self.name, + image_id=image_id, + command=command, + volumes=volumes, + ports=ports, + hostname=hostname, + is_stream_logs=is_stream_logs, + **kwargs + ) + else: + logger.info('You skipped building the image, so image not found.') return self def to_service( From 2615e796593bc7ee1b484eee935323b0ad0c44ef Mon Sep 17 00:00:00 2001 From: WangYunHua-TREC <10154231wang_yunhua@cn.tre-inc.com> Date: Mon, 19 Aug 2024 15:46:47 +0800 Subject: [PATCH 3/9] Add gcp image function(AIGEAR-43) --- example/pipeline/iris_pipeline.py | 15 ++-- src/aigear/deploy/gcp/images.py | 137 ++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 src/aigear/deploy/gcp/images.py diff --git a/example/pipeline/iris_pipeline.py b/example/pipeline/iris_pipeline.py index 751de20..99257e2 100644 --- a/example/pipeline/iris_pipeline.py +++ b/example/pipeline/iris_pipeline.py @@ -85,11 +85,12 @@ def my_pipeline(): skip_build_image=True, cpu_count=cpu_count_train, mem_limit=mem_limit_train - ).to_service( - hostname=hostname, - ports=ports, - volumes=volumes, - tag=service_dir, - cpu_count=cpu_count_service, - mem_limit=mem_limit_service ) + # ).to_service( + # hostname=hostname, + # ports=ports, + # volumes=volumes, + # tag=service_dir, + # cpu_count=cpu_count_service, + # mem_limit=mem_limit_service + # ) diff --git a/src/aigear/deploy/gcp/images.py b/src/aigear/deploy/gcp/images.py new file mode 100644 index 0000000..4932993 --- /dev/null +++ b/src/aigear/deploy/gcp/images.py @@ -0,0 +1,137 @@ +import subprocess +from ..docker.client import docker_client, ImageNotFound +from ...common.logger import logger + + +class ArtifactRegistry: + def __init__(self): + self.rep_exists = True + + def create( + self, + repository: str, + location: str, + description: str, + ): + command = [ + "gcloud", "artifacts", "repositories", "create", repository, + "--repository-format=docker", + f"--location={location}", + f"--description={description}" + ] + self.run_sh( + command=command, + ) + + def describe( + self, + repository: str, + location: str, + ): + command = [ + "gcloud", "artifacts", "repositories", "describe", repository, + f"--location={location}", + ] + self.run_sh( + command=command, + ) + + def docker_auth( + self, + location, + ): + self.run_sh( + command=["gcloud", "auth", "configure-docker", f"{location}-docker.pkg.dev"], + inputs="yes\n", + ) + + def run_sh( + self, + command: list, + inputs: str = None, + ): + result = subprocess.run( + command, + input=inputs, + text=True, + capture_output=True, + shell=True, + ) + event = result.stderr + if "ALREADY_EXISTS" in event: + logger.info("The repository already exists.") + elif "NOT_FOUND" in event: + self.rep_exists = False + logger.error("The repository not found.") + elif "registered correctly" in event: + logger.info("gcloud credential helpers already registered correctly.") + elif "Registry URL" in event: + logger.info("The repository already exists.") + else: + logger.info(event) + + +class ToGCPImage(ArtifactRegistry): + def __init__( + self, + project_id: str, + location: str, + repository: str, + description: str, + ): + super().__init__() + self.project_id = project_id + self.location = location + self.repository = repository + self.description = description + self.gcp_image = None + self.docker_auth(location) + self.describe( + repository=repository, + location=location, + ) + + def tag( + self, + source_image, + gcp_image, + tag=None, + ): + with docker_client() as client: + try: + local_image = client.images.get(source_image) + except ImageNotFound: + logger.info(f'Image not found: {source_image}.') + + if tag is None: + self.gcp_image = f"{self.location}-docker.pkg.dev/{self.project_id}/{self.repository}/{gcp_image}" + else: + self.gcp_image = f"{self.location}-docker.pkg.dev/{self.project_id}/{self.repository}/{gcp_image}:{tag}" + + local_image.tag(self.gcp_image) + return self + + def push(self): + if self.gcp_image is None: + logger.info("The local image is not tagged in artifact registry format.") + return None + if not self.rep_exists: + self.create( + self.repository, + self.location, + self.description, + ) + + with docker_client() as client: + for event in client.images.push(self.gcp_image, stream=True, decode=True): + status = event.get("status") + if "error" in event: + logger.info(event["error"]) + elif "aux" in event: + logger.info(event["aux"]) + elif status in ["Preparing", "Waiting", "Pushing"]: + continue + elif status == "Pushed": + logger.info(event) + else: + logger.info(event) From 32ddb5e98039f68110f60de3686b78bb87edc98b Mon Sep 17 00:00:00 2001 From: WangYunHua-TREC <10154231wang_yunhua@cn.tre-inc.com> Date: Fri, 23 Aug 2024 16:42:30 +0800 Subject: [PATCH 4/9] Add gcp image, pubsub and vm function(AIGEAR-43) --- pyproject.toml | 5 + src/aigear/common/logger.py | 2 +- src/aigear/common/sh.py | 16 ++ src/aigear/deploy/gcp/del.py | 93 +++++++++ src/aigear/deploy/gcp/function.py | 63 ++++++ src/aigear/deploy/gcp/images.py | 18 +- src/aigear/deploy/gcp/pub_sub.py | 51 +++++ src/aigear/deploy/gcp/vm.py | 308 ++++++++++++++++++++++++++++++ src/aigear/pipeline/pipeline.py | 1 + 9 files changed, 544 insertions(+), 13 deletions(-) create mode 100644 src/aigear/common/sh.py create mode 100644 src/aigear/deploy/gcp/del.py create mode 100644 src/aigear/deploy/gcp/function.py create mode 100644 src/aigear/deploy/gcp/pub_sub.py create mode 100644 src/aigear/deploy/gcp/vm.py diff --git a/pyproject.toml b/pyproject.toml index e667b4c..2b0073a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,11 @@ msgrpc = [ "grpcio-health-checking >= 1.56.0", "sentry-sdk >= 1.29.2", ] +gcp = [ + "google-cloud-compute", + "google-cloud-pubsub", + "google-cloud-functions", +] [project.scripts] aigear-msgrpc = "aigear.microservices.grpc.service:main" diff --git a/src/aigear/common/logger.py b/src/aigear/common/logger.py index fdfc97a..d43dda0 100644 --- a/src/aigear/common/logger.py +++ b/src/aigear/common/logger.py @@ -19,7 +19,7 @@ def init_logger(): logger_instance = logging.getLogger(__name__) handler = logging.StreamHandler(sys.stdout) # formatter = JsonFormatter() - formatter = logging.Formatter('%(process)d - %(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter('aigear-%(process)d - %(asctime)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger_instance.addHandler(handler) logger_instance.setLevel(logging.INFO) diff --git a/src/aigear/common/sh.py b/src/aigear/common/sh.py new file mode 100644 index 0000000..fdadb23 --- /dev/null +++ b/src/aigear/common/sh.py @@ -0,0 +1,16 @@ +import subprocess + + +def run_sh( + command: list, + inputs: str = None, +): + result = subprocess.run( + command, + input=inputs, + text=True, + capture_output=True, + shell=True, + ) + event = result.stderr + return event diff --git a/src/aigear/deploy/gcp/del.py b/src/aigear/deploy/gcp/del.py new file mode 100644 index 0000000..80fbd0e --- /dev/null +++ b/src/aigear/deploy/gcp/del.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import sys +from typing import Any + +from google.api_core.extended_operation import ExtendedOperation +from google.cloud import compute_v1 + + +def wait_for_extended_operation( + operation: ExtendedOperation, verbose_name: str = "operation", timeout: int = 300 +) -> Any: + """ + Waits for the extended (long-running) operation to complete. + + If the operation is successful, it will return its result. + If the operation ends with an error, an exception will be raised. + If there were any warnings during the execution of the operation + they will be printed to sys.stderr. + + Args: + operation: a long-running operation you want to wait on. + verbose_name: (optional) a more verbose name of the operation, + used only during error and warning reporting. + timeout: how long (in seconds) to wait for operation to finish. + If None, wait indefinitely. + + Returns: + Whatever the operation.result() returns. + + Raises: + This method will raise the exception received from `operation.exception()` + or RuntimeError if there is no exception set, but there is an `error_code` + set for the `operation`. + + In case of an operation taking longer than `timeout` seconds to complete, + a `concurrent.futures.TimeoutError` will be raised. + """ + result = operation.result(timeout=timeout) + + if operation.error_code: + print( + f"Error during {verbose_name}: [Code: {operation.error_code}]: {operation.error_message}", + file=sys.stderr, + flush=True, + ) + print(f"Operation ID: {operation.name}", file=sys.stderr, flush=True) + raise operation.exception() or RuntimeError(operation.error_message) + + if operation.warnings: + print(f"Warnings during {verbose_name}:\n", file=sys.stderr, flush=True) + for warning in operation.warnings: + print(f" - {warning.code}: {warning.message}", file=sys.stderr, flush=True) + + return result + + +def delete_instance(project_id: str, zone: str, machine_name: str) -> None: + """ + Send an instance deletion request to the Compute Engine API and wait for it to complete. + + Args: + project_id: project ID or project number of the Cloud project you want to use. + zone: name of the zone you want to use. For example: “us-west3-b” + machine_name: name of the machine you want to delete. + """ + instance_client = compute_v1.InstancesClient() + + print(f"Deleting {machine_name} from {zone}...") + operation = instance_client.delete( + project=project_id, zone=zone, instance=machine_name + ) + wait_for_extended_operation(operation, "instance deletion") + print(f"Instance {machine_name} deleted.") + + +if __name__ == "__main__": + import google.auth + import google.auth.exceptions + + try: + default_project_id = google.auth.default()[1] + print(default_project_id) + except google.auth.exceptions.DefaultCredentialsError: + print( + "Please use `gcloud auth application-default login` " + "or set GOOGLE_APPLICATION_CREDENTIALS to use this script." + ) + else: + instance_name = "aigear-test-vm" + instance_zone = "asia-northeast1-a" + + delete_instance(default_project_id, instance_zone, instance_name) diff --git a/src/aigear/deploy/gcp/function.py b/src/aigear/deploy/gcp/function.py new file mode 100644 index 0000000..0840162 --- /dev/null +++ b/src/aigear/deploy/gcp/function.py @@ -0,0 +1,63 @@ +from google.cloud import functions_v2 + + +def get_function_request(configuration, function_name, topic_path): + deployment_settings = configuration.get("deployment_settings", {}) + env_vars = configuration.get("env_vars", {}) + project_id = env_vars.get("GCP_PROJECT_ID") + + print(f"Deployment Settings:\n{deployment_settings}") + print(f"Env vars:\n{env_vars}") + region = deployment_settings.get("region", "us-central1") + parent = f"projects/{project_id}/locations/{region}" + function_id = function_name.lower() + bucket_name = deployment_settings.get("bucket_name") + source_code_path = deployment_settings.get("source_code_path") + + project_id = 'ssc-ape-staging' + topic = 'aigear-test' + topic_path = f'projects/{project_id}/topics/{topic}' + + storage_source = functions_v2.types.StorageSource( + bucket=bucket_name, + object_=source_code_path + ) + + source = functions_v2.types.Source( + storage_source=storage_source + ) + + build_config = functions_v2.types.BuildConfig( + entry_point="main", + runtime="python38", + source=source + ) + event_trigger = functions_v2.types.EventTrigger( + event_type="google.cloud.pubsub.topic.v1.messagePublished", + pubsub_topic=topic_path, + retry_policy=functions_v2.types.EventTrigger.RetryPolicy.RETRY_POLICY_RETRY + ) + service_config = functions_v2.types.ServiceConfig( + vpc_connector=deployment_settings.get("vpc_connector"), + service_account_email=deployment_settings.get("service_account"), + environment_variables=env_vars, + available_cpu=deployment_settings.get("cpu", "1"), + min_instance_count=deployment_settings.get("min_instances", 0), + max_instance_count=deployment_settings.get("max_instances", 2), + max_instance_request_concurrency=deployment_settings.get("concurrency", 4), + timeout_seconds=120 + ) + # Define the function + function = functions_v2.types.Function( + name=f"{parent}/functions/{function_id}", + description="A serverless dispatcher", + build_config=build_config, + event_trigger=event_trigger, + service_config=service_config + ) + request = functions_v2.CreateFunctionRequest( + parent=parent, + function=function, + function_id=function_id + ) + return request diff --git a/src/aigear/deploy/gcp/images.py b/src/aigear/deploy/gcp/images.py index 4932993..6f9b347 100644 --- a/src/aigear/deploy/gcp/images.py +++ b/src/aigear/deploy/gcp/images.py @@ -1,6 +1,7 @@ import subprocess from ..docker.client import docker_client, ImageNotFound from ...common.logger import logger +from ...common.sh import run_sh class ArtifactRegistry: @@ -19,7 +20,7 @@ def create( f"--location={location}", f"--description={description}" ] - self.run_sh( + self._run_sh( command=command, ) @@ -32,7 +33,7 @@ def describe( "gcloud", "artifacts", "repositories", "describe", repository, f"--location={location}", ] - self.run_sh( + self._run_sh( command=command, ) @@ -40,24 +41,17 @@ def docker_auth( self, location, ): - self.run_sh( + self._run_sh( command=["gcloud", "auth", "configure-docker", f"{location}-docker.pkg.dev"], inputs="yes\n", ) - def run_sh( + def _run_sh( self, command: list, inputs: str = None, ): - result = subprocess.run( - command, - input=inputs, - text=True, - capture_output=True, - shell=True, - ) - event = result.stderr + event = run_sh(command, inputs) if "ALREADY_EXISTS" in event: logger.info("The repository already exists.") elif "NOT_FOUND" in event: diff --git a/src/aigear/deploy/gcp/pub_sub.py b/src/aigear/deploy/gcp/pub_sub.py new file mode 100644 index 0000000..adeb799 --- /dev/null +++ b/src/aigear/deploy/gcp/pub_sub.py @@ -0,0 +1,51 @@ +from ...common.logger import logger +from ...common.sh import run_sh + + +class PubSub: + def __init__(self, topic_name: str): + self.topic_name = topic_name + + def create(self): + command = [ + "gcloud", "pubsub", "topics", "create", self.topic_name + ] + event = run_sh(command) + logger.info(event) + + def describe(self): + is_exist = False + command = [ + "gcloud", "pubsub", "topics", "describe", self.topic_name + ] + event = run_sh(command) + if "name: projects" in event: + is_exist = True + logger.info(f"Find resources: {event}") + elif "NOT_FOUND" in event: + logger.info(f"NOT_FOUND: Resource not found (resource={self.topic_name})") + else: + logger.info(event) + return is_exist + + def delete(self): + command = [ + "gcloud", "pubsub", "topics", "delete", self.topic_name + ] + event = run_sh(command) + logger.info(event) + + def list(self): + command = [ + "gcloud", "pubsub", "topics", "list", f"--filter=name.scope(topic):{self.topic_name}" + ] + event = run_sh(command) + logger.info(event) + + def publish(self, message): + command = [ + "gcloud", "pubsub", "topics", "publish", self.topic_name, + f"--message = {message}", + ] + event = run_sh(command) + logger.info(event) diff --git a/src/aigear/deploy/gcp/vm.py b/src/aigear/deploy/gcp/vm.py new file mode 100644 index 0000000..a7a53b6 --- /dev/null +++ b/src/aigear/deploy/gcp/vm.py @@ -0,0 +1,308 @@ +# [START compute_instances_create] +from __future__ import annotations + +import re +import sys +from typing import Any +import warnings + +from google.api_core.extended_operation import ExtendedOperation +from collections.abc import Iterable +from google.cloud import compute_v1 + + +def get_image_from_family(project: str, family: str) -> compute_v1.Image: + """ + Retrieve the newest image that is part of a given family in a project. + + Args: + project: project ID or project number of the Cloud project you want to get image from. + family: name of the image family you want to get image from. + + Returns: + An Image object. + """ + image_client = compute_v1.ImagesClient() + # List of public operating system (OS) images: https://cloud.google.com/compute/docs/images/os-details + newest_image = image_client.get_from_family(project=project, family=family) + return newest_image + + +def disk_from_image( + disk_type: str, + disk_size_gb: int, + boot: bool, + source_image: str, + auto_delete: bool = True, +) -> compute_v1.AttachedDisk: + """ + Create an AttachedDisk object to be used in VM instance creation. Uses an image as the + source for the new disk. + + Args: + disk_type: the type of disk you want to create. This value uses the following format: + "zones/{zone}/diskTypes/(pd-standard|pd-ssd|pd-balanced|pd-extreme)". + For example: "zones/us-west3-b/diskTypes/pd-ssd" + disk_size_gb: size of the new disk in gigabytes + boot: boolean flag indicating whether this disk should be used as a boot disk of an instance + source_image: source image to use when creating this disk. You must have read access to this disk. This can be one + of the publicly available images or an image from one of your projects. + This value uses the following format: "projects/{project_name}/global/images/{image_name}" + auto_delete: boolean flag indicating whether this disk should be deleted with the VM that uses it + + Returns: + AttachedDisk object configured to be created using the specified image. + """ + boot_disk = compute_v1.AttachedDisk() + initialize_params = compute_v1.AttachedDiskInitializeParams() + initialize_params.source_image = source_image + initialize_params.disk_size_gb = disk_size_gb + initialize_params.disk_type = disk_type + boot_disk.initialize_params = initialize_params + # Remember to set auto_delete to True if you want the disk to be deleted when you delete + # your VM instance. + boot_disk.auto_delete = auto_delete + boot_disk.boot = boot + return boot_disk + + +def wait_for_extended_operation( + operation: ExtendedOperation, verbose_name: str = "operation", timeout: int = 300 +) -> Any: + """ + Waits for the extended (long-running) operation to complete. + + If the operation is successful, it will return its result. + If the operation ends with an error, an exception will be raised. + If there were any warnings during the execution of the operation + they will be printed to sys.stderr. + + Args: + operation: a long-running operation you want to wait on. + verbose_name: (optional) a more verbose name of the operation, + used only during error and warning reporting. + timeout: how long (in seconds) to wait for operation to finish. + If None, wait indefinitely. + + Returns: + Whatever the operation.result() returns. + + Raises: + This method will raise the exception received from `operation.exception()` + or RuntimeError if there is no exception set, but there is an `error_code` + set for the `operation`. + + In case of an operation taking longer than `timeout` seconds to complete, + a `concurrent.futures.TimeoutError` will be raised. + """ + result = operation.result(timeout=timeout) + + if operation.error_code: + print( + f"Error during {verbose_name}: [Code: {operation.error_code}]: {operation.error_message}", + file=sys.stderr, + flush=True, + ) + print(f"Operation ID: {operation.name}", file=sys.stderr, flush=True) + raise operation.exception() or RuntimeError(operation.error_message) + + if operation.warnings: + print(f"Warnings during {verbose_name}:\n", file=sys.stderr, flush=True) + for warning in operation.warnings: + print(f" - {warning.code}: {warning.message}", file=sys.stderr, flush=True) + + return result + + +def create_instance( + project_id: str, + zone: str, + instance_name: str, + disks: list[compute_v1.AttachedDisk], + machine_type: str = "n1-standard-1", + network_link: str = "global/networks/default", + subnetwork_link: str = None, + internal_ip: str = None, + external_access: bool = False, + external_ipv4: str = None, + accelerators: list[compute_v1.AcceleratorConfig] = None, + preemptible: bool = False, + spot: bool = False, + instance_termination_action: str = "STOP", + custom_hostname: str = None, + delete_protection: bool = False, +) -> compute_v1.Instance: + """ + Send an instance creation request to the Compute Engine API and wait for it to complete. + + Args: + project_id: project ID or project number of the Cloud project you want to use. + zone: name of the zone to create the instance in. For example: "us-west3-b" + instance_name: name of the new virtual machine (VM) instance. + disks: a list of compute_v1.AttachedDisk objects describing the disks + you want to attach to your new instance. + machine_type: machine type of the VM being created. This value uses the + following format: "zones/{zone}/machineTypes/{type_name}". + For example: "zones/europe-west3-c/machineTypes/f1-micro" + network_link: name of the network you want the new instance to use. + For example: "global/networks/default" represents the network + named "default", which is created automatically for each project. + subnetwork_link: name of the subnetwork you want the new instance to use. + This value uses the following format: + "regions/{region}/subnetworks/{subnetwork_name}" + internal_ip: internal IP address you want to assign to the new instance. + By default, a free address from the pool of available internal IP addresses of + used subnet will be used. + external_access: boolean flag indicating if the instance should have an external IPv4 + address assigned. + external_ipv4: external IPv4 address to be assigned to this instance. If you specify + an external IP address, it must live in the same region as the zone of the instance. + This setting requires `external_access` to be set to True to work. + accelerators: a list of AcceleratorConfig objects describing the accelerators that will + be attached to the new instance. + preemptible: boolean value indicating if the new instance should be preemptible + or not. Preemptible VMs have been deprecated and you should now use Spot VMs. + spot: boolean value indicating if the new instance should be a Spot VM or not. + instance_termination_action: What action should be taken once a Spot VM is terminated. + Possible values: "STOP", "DELETE" + custom_hostname: Custom hostname of the new VM instance. + Custom hostnames must conform to RFC 1035 requirements for valid hostnames. + delete_protection: boolean value indicating if the new virtual machine should be + protected against deletion or not. + Returns: + Instance object. + """ + instance_client = compute_v1.InstancesClient() + + # Use the network interface provided in the network_link argument. + network_interface = compute_v1.NetworkInterface() + network_interface.network = network_link + if subnetwork_link: + network_interface.subnetwork = subnetwork_link + + if internal_ip: + network_interface.network_i_p = internal_ip + + if external_access: + access = compute_v1.AccessConfig() + access.type_ = compute_v1.AccessConfig.Type.ONE_TO_ONE_NAT.name + access.name = "External NAT" + access.network_tier = access.NetworkTier.PREMIUM.name + if external_ipv4: + access.nat_i_p = external_ipv4 + network_interface.access_configs = [access] + + # Collect information into the Instance object. + instance = compute_v1.Instance() + instance.network_interfaces = [network_interface] + instance.name = instance_name + instance.disks = disks + if re.match(r"^zones/[a-z\d\-]+/machineTypes/[a-z\d\-]+$", machine_type): + instance.machine_type = machine_type + else: + instance.machine_type = f"zones/{zone}/machineTypes/{machine_type}" + + instance.scheduling = compute_v1.Scheduling() + if accelerators: + instance.guest_accelerators = accelerators + instance.scheduling.on_host_maintenance = ( + compute_v1.Scheduling.OnHostMaintenance.TERMINATE.name + ) + + if preemptible: + # Set the preemptible setting + warnings.warn( + "Preemptible VMs are being replaced by Spot VMs.", DeprecationWarning + ) + instance.scheduling = compute_v1.Scheduling() + instance.scheduling.preemptible = True + + if spot: + # Set the Spot VM setting + instance.scheduling.provisioning_model = ( + compute_v1.Scheduling.ProvisioningModel.SPOT.name + ) + instance.scheduling.instance_termination_action = instance_termination_action + + if custom_hostname is not None: + # Set the custom hostname for the instance + instance.hostname = custom_hostname + + if delete_protection: + # Set the delete protection bit + instance.deletion_protection = True + + # Prepare the request to insert an instance. + request = compute_v1.InsertInstanceRequest() + request.zone = zone + request.project = project_id + request.instance_resource = instance + + # Wait for the create operation to complete. + print(f"Creating the {instance_name} instance in {zone}...") + + operation = instance_client.insert(request=request) + + wait_for_extended_operation(operation, "instance creation") + + print(f"Instance {instance_name} created.") + return instance_client.get(project=project_id, zone=zone, instance=instance_name) + + +def list_instances(project_id: str, zone: str) -> Iterable[compute_v1.Instance]: + """ + List all instances in the given zone in the specified project. + + Args: + project_id: project ID or project number of the Cloud project you want to use. + zone: name of the zone you want to use. For example: “us-west3-b” + Returns: + An iterable collection of Instance objects. + """ + instance_client = compute_v1.InstancesClient() + instance_list = instance_client.list(project=project_id, zone=zone) + + print(f"Instances found in zone {zone}:") + for instance in instance_list: + print(f" - {instance.name} ({instance.machine_type})") + + +if __name__ == "__main__": + import google.auth + import google.auth.exceptions + + try: + default_project_id = google.auth.default()[1] + print(default_project_id) + except google.auth.exceptions.DefaultCredentialsError: + print( + "Please use `gcloud auth application-default login` " + "or set GOOGLE_APPLICATION_CREDENTIALS to use this script." + ) + else: + instance_name = "aigear-test-vm" + instance_zone = "asia-northeast1-a" + + newest_debian = get_image_from_family( + project="debian-cloud", family="debian-12" + ) + print(newest_debian.self_link) + disk_type = f"zones/{instance_zone}/diskTypes/pd-standard" + disks = [disk_from_image(disk_type, 10, True, newest_debian.self_link)] + machine_type = "n1-standard-1" + external_access = True + + # response = create_instance( + # default_project_id, + # instance_zone, + # instance_name, + # disks, + # machine_type, + # external_access=external_access + # ) + # print(response) + + list_instances(default_project_id, instance_zone) + +# gcloud compute ssh --project=ssc-ape-staging --zone=asia-northeast1-a aigear-test-vm +# gcloud compute instances create-with-container test-vm --container-image gcr.io/cloud-marketplace/google/nginx1:latest \ No newline at end of file diff --git a/src/aigear/pipeline/pipeline.py b/src/aigear/pipeline/pipeline.py index adc83e4..e9ab2d9 100644 --- a/src/aigear/pipeline/pipeline.py +++ b/src/aigear/pipeline/pipeline.py @@ -64,6 +64,7 @@ def deploy( if image_id: flow_path = flow_path_in_workdir(self._flow_file) command = f"aigear-workflow --script_path {flow_path} --function_name {self.fn.__name__}" + # TODO: Next optimization, the container name should include the version(self.version) run_or_restart_container( container_name=self.name, image_id=image_id, From 247141e2cad347d2010015c98a38ee9cd4f240de Mon Sep 17 00:00:00 2001 From: WangYunHua-TREC <10154231wang_yunhua@cn.tre-inc.com> Date: Mon, 26 Aug 2024 15:19:17 +0800 Subject: [PATCH 5/9] Add Subscriptions(AIGEAR-43) --- src/aigear/common/sh.py | 7 ++-- src/aigear/deploy/gcp/pub_sub.py | 57 ++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/aigear/common/sh.py b/src/aigear/common/sh.py index fdadb23..2ef2c4f 100644 --- a/src/aigear/common/sh.py +++ b/src/aigear/common/sh.py @@ -12,5 +12,8 @@ def run_sh( capture_output=True, shell=True, ) - event = result.stderr - return event + stderr = result.stderr + if stderr: + return stderr + else: + return result.stdout diff --git a/src/aigear/deploy/gcp/pub_sub.py b/src/aigear/deploy/gcp/pub_sub.py index adeb799..c82d303 100644 --- a/src/aigear/deploy/gcp/pub_sub.py +++ b/src/aigear/deploy/gcp/pub_sub.py @@ -2,7 +2,7 @@ from ...common.sh import run_sh -class PubSub: +class Publish: def __init__(self, topic_name: str): self.topic_name = topic_name @@ -45,7 +45,60 @@ def list(self): def publish(self, message): command = [ "gcloud", "pubsub", "topics", "publish", self.topic_name, - f"--message = {message}", + f"--message={message}", + ] + event = run_sh(command) + logger.info(event) + + +class Subscriptions: + def __init__(self, sub_name: str, topic_name: str): + self.sub_name = sub_name + self.topic_name = topic_name + + def create(self): + command = [ + "gcloud", "pubsub", "subscriptions", "create", self.sub_name, + f"--topic={self.topic_name}", + ] + event = run_sh(command) + logger.info(event) + + def describe(self): + is_exist = False + command = [ + "gcloud", "pubsub", "subscriptions", "describe", self.sub_name + ] + event = run_sh(command) + if "name: projects" in event: + is_exist = True + logger.info(f"Find resources: {event}") + elif "NOT_FOUND" in event: + logger.info(f"NOT_FOUND: Resource not found (resource={self.sub_name})") + else: + logger.info(event) + return is_exist + + def delete(self): + command = [ + "gcloud", "pubsub", "subscriptions", "delete", self.sub_name + ] + event = run_sh(command) + logger.info(event) + + @staticmethod + def list(): + command = [ + "gcloud", "pubsub", "subscriptions", "list" + ] + event = run_sh(command) + logger.info(event) + + def pull(self): + command = [ + "gcloud", "pubsub", "subscriptions", "pull", self.sub_name, + "--format=json(ackId,message.attributes,message.data.decode(\"base64\").decode(\"utf-8\")," + "message.messageId,message.publishTime)" ] event = run_sh(command) logger.info(event) From 5b05febc1670e3a21b6d09b33b02c47021db5c01 Mon Sep 17 00:00:00 2001 From: WangYunHua-TREC <10154231wang_yunhua@cn.tre-inc.com> Date: Tue, 27 Aug 2024 16:57:20 +0800 Subject: [PATCH 6/9] Add loud function(AIGEAR-48) --- src/aigear/deploy/gcp/function.py | 132 +++++++++------- src/aigear/deploy/gcp/function/index.js | 158 ++++++++++++++++++++ src/aigear/deploy/gcp/function/package.json | 9 ++ 3 files changed, 243 insertions(+), 56 deletions(-) create mode 100644 src/aigear/deploy/gcp/function/index.js create mode 100644 src/aigear/deploy/gcp/function/package.json diff --git a/src/aigear/deploy/gcp/function.py b/src/aigear/deploy/gcp/function.py index 0840162..611106c 100644 --- a/src/aigear/deploy/gcp/function.py +++ b/src/aigear/deploy/gcp/function.py @@ -1,63 +1,83 @@ -from google.cloud import functions_v2 +from pathlib import Path +from ...common.logger import logger +from ...common.sh import run_sh -def get_function_request(configuration, function_name, topic_path): - deployment_settings = configuration.get("deployment_settings", {}) - env_vars = configuration.get("env_vars", {}) - project_id = env_vars.get("GCP_PROJECT_ID") +class CloudFunction: + def __init__( + self, + function_name, + region, + entry_point, + topic_name, + ): + self.function_name = function_name + self.region = region + self.entry_point = entry_point + self.topic_name = topic_name + self.source_path = Path(__file__).resolve().parent / "function" - print(f"Deployment Settings:\n{deployment_settings}") - print(f"Env vars:\n{env_vars}") - region = deployment_settings.get("region", "us-central1") - parent = f"projects/{project_id}/locations/{region}" - function_id = function_name.lower() - bucket_name = deployment_settings.get("bucket_name") - source_code_path = deployment_settings.get("source_code_path") + def deploy(self): + command = [ + "gcloud", "functions", "deploy", + self.function_name, + "--gen2", + "--runtime=nodejs20", + "--allow-unauthenticated", + f"--region={self.region}", + f"--entry-point={self.entry_point}", + f"--trigger-topic={self.topic_name}", + f"--source={self.source_path}", + ] + event = run_sh(command) + logger.info(event) + if "ERROR" in event: + logger.info("Error occurred while creating cloud function.") - project_id = 'ssc-ape-staging' - topic = 'aigear-test' - topic_path = f'projects/{project_id}/topics/{topic}' + def logs(self, limit=5): + command = [ + "gcloud", "functions", "logs", "read", + "--gen2", + f"--region={self.region}", + f"--limit={limit}", + self.function_name, + ] + event = run_sh(command) + logger.info(event) - storage_source = functions_v2.types.StorageSource( - bucket=bucket_name, - object_=source_code_path - ) + def describe(self): + is_exist = False + command = [ + "gcloud", "functions", "describe", + self.function_name, + f"--region={self.region}", + ] + event = run_sh(command) + if "ACTIVE" in event: + is_exist = True + logger.info(f"Find resources: {event}") + elif "ERROR" in event and "not found" in event: + logger.info(f"NOT_FOUND: Resource not found: {event}") + else: + logger.info(event) + return is_exist - source = functions_v2.types.Source( - storage_source=storage_source - ) + def list(self): + command = [ + "gcloud", "functions", "list", + f"--regions={self.region}", + "--v2", + f"--filter={self.function_name}", + ] + event = run_sh(command) + logger.info(f"\n{event}") - build_config = functions_v2.types.BuildConfig( - entry_point="main", - runtime="python38", - source=source - ) - event_trigger = functions_v2.types.EventTrigger( - event_type="google.cloud.pubsub.topic.v1.messagePublished", - pubsub_topic=topic_path, - retry_policy=functions_v2.types.EventTrigger.RetryPolicy.RETRY_POLICY_RETRY - ) - service_config = functions_v2.types.ServiceConfig( - vpc_connector=deployment_settings.get("vpc_connector"), - service_account_email=deployment_settings.get("service_account"), - environment_variables=env_vars, - available_cpu=deployment_settings.get("cpu", "1"), - min_instance_count=deployment_settings.get("min_instances", 0), - max_instance_count=deployment_settings.get("max_instances", 2), - max_instance_request_concurrency=deployment_settings.get("concurrency", 4), - timeout_seconds=120 - ) - # Define the function - function = functions_v2.types.Function( - name=f"{parent}/functions/{function_id}", - description="A serverless dispatcher", - build_config=build_config, - event_trigger=event_trigger, - service_config=service_config - ) - request = functions_v2.CreateFunctionRequest( - parent=parent, - function=function, - function_id=function_id - ) - return request + def delete(self): + command = [ + "gcloud", "functions", "delete", + self.function_name, + "--gen2", + f"--region={self.region}", + ] + event = run_sh(command, "yes\n") + logger.info(f"\n{event}") diff --git a/src/aigear/deploy/gcp/function/index.js b/src/aigear/deploy/gcp/function/index.js new file mode 100644 index 0000000..2bdec9f --- /dev/null +++ b/src/aigear/deploy/gcp/function/index.js @@ -0,0 +1,158 @@ + +/** + * Triggered from a message on a Cloud Pub/Sub topic. + * + * @param {!Object} event Event payload. + * @param {!Object} context Metadata for the event. + */ +const Buffer = require('safe-buffer').Buffer; +const Compute = require('@google-cloud/compute'); +const compute = new Compute(); +// Change this const value to your project +const projectId = 'ssc-ape-staging'; +const zone = 'asia-northeast1-a'; + +//Build environment and clean up +const commonCommand = 'cd /var\ngcloud auth configure-docker asia-northeast1-docker.pkg.dev --quiet\nsudo docker pull ${dockerImage}\nsudo docker run ${dockerImage}\ndocker_exit_code=$?\n[ ${docker_exit_code} -eq "0" ] && gcloud pubsub topics publish trial_rankfm_ape_pubsub --message \'createVMDate\' || gcloud pubsub topics publish trial_rankfm_ape_pubsub --message "Exit code: ${docker_exit_code}"\ngcp_zone=$(curl -H Metadata-Flavor:Google http://metadata.google.internal/computeMetadata/v1/instance/zone -s | cut -d/ -f4)\nsleep 300\nhostname_result=$(hostname)\nextracted_name=$(echo ${hostname_result} | cut -d. -f1)\ngcloud compute instances delete ${extracted_name} --zone ${gcp_zone}'; + + +const vmConfig = { + kind: 'compute#instance', + name: 'aigear-vm', + zone: `projects/${projectId}/zones/${zone}`, + machineType: `projects/${projectId}/zones/${zone}/machineTypes/`, + displayDevice: { + enableDisplay: false + }, + metadata: { + kind: 'compute#metadata', + items: [ + { + key: 'startup-script', + value: commonCommand + } + ] + }, + tags: { + items: [] + }, + disks: [ + { + kind: 'compute#attachedDisk', + type: 'PERSISTENT', + boot: true, + mode: 'READ_WRITE', + autoDelete: true, + deviceName: 'aigear-vm', + initializeParams: { + sourceImage: `projects/${projectId}/global/images/ml-model-training-cloud-function-image`, + diskType: `projects/${projectId}/zones/${zone}/diskTypes/pd-standard`, + diskSizeGb: '20' + }, + diskEncryptionKey: {} + } + ], + canIpForward: false, + networkInterfaces: [ + { + kind: 'compute#networkInterface', + subnetwork: `projects/${projectId}/regions/asia-northeast1/subnetworks/default`, + accessConfigs: [ + { + kind: 'compute#accessConfig', + name: 'External NAT', + type: 'ONE_TO_ONE_NAT', + networkTier: 'PREMIUM' + } + ], + aliasIpRanges: [] + } + ], + description: '', + labels: {}, + scheduling: { + preemptible: false, + onHostMaintenance: 'MIGRATE', + automaticRestart: true, + nodeAffinities: [] + }, + deletionProtection: false, + reservationAffinity: { + consumeReservationType: 'ANY_RESERVATION' + }, + serviceAccounts: [ + { + email: `200251827214-compute@developer.gserviceaccount.com`, + scopes: [ + 'https://www.googleapis.com/auth/cloud-platform' + ] + } + ], + shieldedInstanceConfig: { + enableSecureBoot: false, + enableVtpm: true, + enableIntegrityMonitoring: true + }, + confidentialInstanceConfig: { + enableConfidentialCompute: false + } +} +const functions = require('@google-cloud/functions-framework'); + +// Register a CloudEvent callback with the Functions Framework that will +// be executed when the Pub/Sub trigger topic receives a message. +functions.cloudEvent('cronjobProcessPubSub', cloudEvent => { + // The Pub/Sub message is passed as the CloudEvent's data payload. + const message = Buffer.from(cloudEvent.data.message.data, 'base64').toString(); + const cronjobInfo = JSON.parse(message); + if(cronjobInfo.length == 0) { + return; + } + console.log(`vmName is ${cronjobInfo[0].vmName}`); + console.log(`command is ${cronjobInfo[0].command}`); + console.log(`cronjobInfo.spec is ${cronjobInfo[0].spec}`); + + // set vm spec + const spec = cronjobInfo[0].spec ? cronjobInfo[0].spec : 'e2-medium'; + const machineTypeSpec = `projects/${projectId}/zones/${zone}/machineTypes/` + spec; + console.log(`machineTypeSpec is ${machineTypeSpec}`); + + vmConfig.machineType = machineTypeSpec; + + // VM and hard disk name + vmConfig.name = cronjobInfo[0].vmName; + vmConfig.disks[0].deviceName = cronjobInfo[0].vmName; + const diskSizeGb = cronjobInfo[0].diskSizeGb ? cronjobInfo[0].diskSizeGb : '20'; + vmConfig.disks[0].initializeParams.diskSizeGb = diskSizeGb; + vmConfig.scheduling.onHostMaintenance = cronjobInfo[0].onHostMaintenance; + const vmName = cronjobInfo[0].vmName + Date.now(); + const dockerImage = cronjobInfo[0].dockerImage; + console.log(JSON.stringify(cronjobInfo)); + cronjobInfo.shift() + + vmConfig.metadata.items[0].value = commonCommand.replace(/\${dockerImage}/g,dockerImage).replace('createVMDate',JSON.stringify(cronjobInfo)); + + + try { + compute.zone(zone) + .createVM(vmName, vmConfig) + .then(data => { + // Operation pending. + const vm = data[0]; + const operation = data[1]; + console.log(`VM being created: ${vm.id}`); + console.log(`Operation info: ${operation.id}`); + return operation.promise(); + }) + .then(() => { + const message = 'VM created with success, Cloud Function finished execution.'; + console.log(message); + }) + .catch(err => { + console.log(err); + }); + + } catch (err) { + console.log(err); + } +}); diff --git a/src/aigear/deploy/gcp/function/package.json b/src/aigear/deploy/gcp/function/package.json new file mode 100644 index 0000000..5d9464d --- /dev/null +++ b/src/aigear/deploy/gcp/function/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@google-cloud/functions-framework": "^3.0.0", + "@google-cloud/compute": "2.4.1", + "@google-cloud/pubsub": "^0.18.0", + "ssh2": "^0.6.0", + "safe-buffer": "^5.1.2" + } +} \ No newline at end of file From 2d4660850b9ba347b9a4259c58c8982a1d9380b8 Mon Sep 17 00:00:00 2001 From: WangYunHua-TREC <10154231wang_yunhua@cn.tre-inc.com> Date: Wed, 28 Aug 2024 15:49:27 +0800 Subject: [PATCH 7/9] Add gcp infra function(AIGEAR-49) --- src/aigear/deploy/gcp/del.py | 93 --------- src/aigear/deploy/gcp/function.py | 3 +- src/aigear/deploy/gcp/iam.py | 21 ++ src/aigear/deploy/gcp/images.py | 1 - src/aigear/deploy/gcp/infra.py | 60 ++++++ src/aigear/deploy/gcp/project.py | 17 ++ src/aigear/deploy/gcp/vm.py | 308 ------------------------------ 7 files changed, 99 insertions(+), 404 deletions(-) delete mode 100644 src/aigear/deploy/gcp/del.py create mode 100644 src/aigear/deploy/gcp/iam.py create mode 100644 src/aigear/deploy/gcp/infra.py create mode 100644 src/aigear/deploy/gcp/project.py delete mode 100644 src/aigear/deploy/gcp/vm.py diff --git a/src/aigear/deploy/gcp/del.py b/src/aigear/deploy/gcp/del.py deleted file mode 100644 index 80fbd0e..0000000 --- a/src/aigear/deploy/gcp/del.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import annotations - -import sys -from typing import Any - -from google.api_core.extended_operation import ExtendedOperation -from google.cloud import compute_v1 - - -def wait_for_extended_operation( - operation: ExtendedOperation, verbose_name: str = "operation", timeout: int = 300 -) -> Any: - """ - Waits for the extended (long-running) operation to complete. - - If the operation is successful, it will return its result. - If the operation ends with an error, an exception will be raised. - If there were any warnings during the execution of the operation - they will be printed to sys.stderr. - - Args: - operation: a long-running operation you want to wait on. - verbose_name: (optional) a more verbose name of the operation, - used only during error and warning reporting. - timeout: how long (in seconds) to wait for operation to finish. - If None, wait indefinitely. - - Returns: - Whatever the operation.result() returns. - - Raises: - This method will raise the exception received from `operation.exception()` - or RuntimeError if there is no exception set, but there is an `error_code` - set for the `operation`. - - In case of an operation taking longer than `timeout` seconds to complete, - a `concurrent.futures.TimeoutError` will be raised. - """ - result = operation.result(timeout=timeout) - - if operation.error_code: - print( - f"Error during {verbose_name}: [Code: {operation.error_code}]: {operation.error_message}", - file=sys.stderr, - flush=True, - ) - print(f"Operation ID: {operation.name}", file=sys.stderr, flush=True) - raise operation.exception() or RuntimeError(operation.error_message) - - if operation.warnings: - print(f"Warnings during {verbose_name}:\n", file=sys.stderr, flush=True) - for warning in operation.warnings: - print(f" - {warning.code}: {warning.message}", file=sys.stderr, flush=True) - - return result - - -def delete_instance(project_id: str, zone: str, machine_name: str) -> None: - """ - Send an instance deletion request to the Compute Engine API and wait for it to complete. - - Args: - project_id: project ID or project number of the Cloud project you want to use. - zone: name of the zone you want to use. For example: “us-west3-b” - machine_name: name of the machine you want to delete. - """ - instance_client = compute_v1.InstancesClient() - - print(f"Deleting {machine_name} from {zone}...") - operation = instance_client.delete( - project=project_id, zone=zone, instance=machine_name - ) - wait_for_extended_operation(operation, "instance deletion") - print(f"Instance {machine_name} deleted.") - - -if __name__ == "__main__": - import google.auth - import google.auth.exceptions - - try: - default_project_id = google.auth.default()[1] - print(default_project_id) - except google.auth.exceptions.DefaultCredentialsError: - print( - "Please use `gcloud auth application-default login` " - "or set GOOGLE_APPLICATION_CREDENTIALS to use this script." - ) - else: - instance_name = "aigear-test-vm" - instance_zone = "asia-northeast1-a" - - delete_instance(default_project_id, instance_zone, instance_name) diff --git a/src/aigear/deploy/gcp/function.py b/src/aigear/deploy/gcp/function.py index 611106c..ea9f1b2 100644 --- a/src/aigear/deploy/gcp/function.py +++ b/src/aigear/deploy/gcp/function.py @@ -15,7 +15,7 @@ def __init__( self.region = region self.entry_point = entry_point self.topic_name = topic_name - self.source_path = Path(__file__).resolve().parent / "function" + self.source_path = Path(__file__).resolve().parent / "function_test" def deploy(self): command = [ @@ -23,7 +23,6 @@ def deploy(self): self.function_name, "--gen2", "--runtime=nodejs20", - "--allow-unauthenticated", f"--region={self.region}", f"--entry-point={self.entry_point}", f"--trigger-topic={self.topic_name}", diff --git a/src/aigear/deploy/gcp/iam.py b/src/aigear/deploy/gcp/iam.py new file mode 100644 index 0000000..0e1b900 --- /dev/null +++ b/src/aigear/deploy/gcp/iam.py @@ -0,0 +1,21 @@ +from ...common.logger import logger +from ...common.sh import run_sh + + +def check_iam(project_id: str): + is_owner = False + command = [ + "gcloud", "projects", "get-iam-policy", + project_id, + "--flatten=bindings[].members", + "--format=table(bindings.role)", + "--filter=bindings.members:$(gcloud config get-value account)", + ] + event = run_sh(command) + if "roles/owner" in event: + is_owner = True + elif event == "": + logger.info("The currently logged in GCP account does not have owner privileges.") + else: + logger.info(event) + return is_owner diff --git a/src/aigear/deploy/gcp/images.py b/src/aigear/deploy/gcp/images.py index 6f9b347..3923088 100644 --- a/src/aigear/deploy/gcp/images.py +++ b/src/aigear/deploy/gcp/images.py @@ -1,4 +1,3 @@ -import subprocess from ..docker.client import docker_client, ImageNotFound from ...common.logger import logger from ...common.sh import run_sh diff --git a/src/aigear/deploy/gcp/infra.py b/src/aigear/deploy/gcp/infra.py new file mode 100644 index 0000000..5a9df66 --- /dev/null +++ b/src/aigear/deploy/gcp/infra.py @@ -0,0 +1,60 @@ +from .function import CloudFunction +from .pub_sub import Publish, Subscriptions +from ...common.logger import logger +from .iam import check_iam +from .project import get_project_id + + +class Infra: + def __init__( + self, + topic_name, + function_name, + region, + ): + sub_name = f"{topic_name}_sub" + _entry_point = "helloPubSub" + + self.pub = Publish(topic_name) + self.sub = Subscriptions(sub_name, topic_name) + self.cf = CloudFunction( + function_name, + region, + _entry_point, + topic_name, + ) + self._check = self._check_infra(self.pub, self.sub, self.cf) + + @staticmethod + def _check_infra(pub, sub, cf): + pub_exist = pub.describe() + if not pub_exist: + logger.info("Pub(pubsub) not created.") + sub_exist = sub.describe() + if not sub_exist: + logger.info("Sub(pubsub) not created.") + cf_exist = cf.describe() + if not cf_exist: + logger.info("Cloud function not created.") + return pub_exist, sub_exist, cf_exist + + def create(self): + pub_exist, sub_exist, cf_exist = self._check + if not pub_exist: + self.pub.create() + + if not sub_exist: + self.sub.create() + + if not cf_exist: + self.cf.deploy() + + def delete(self): + project_id = get_project_id() + owner_pm = check_iam(project_id) + if owner_pm: + self.pub.delete() + self.sub.delete() + self.cf.delete() + else: + logger.info("You are not the owner and cannot delete aigear infrastructure.") diff --git a/src/aigear/deploy/gcp/project.py b/src/aigear/deploy/gcp/project.py new file mode 100644 index 0000000..2d0a125 --- /dev/null +++ b/src/aigear/deploy/gcp/project.py @@ -0,0 +1,17 @@ +from ...common.logger import logger +from ...common.sh import run_sh + + +def get_project_id(): + project_id = None + command = [ + "gcloud", "config", "get-value", "project" + ] + event = run_sh(command) + if "unset" in event: + logger.info("No project id set.") + elif "ERROR" in event: + logger.info(event) + else: + project_id = event.strip() + return project_id diff --git a/src/aigear/deploy/gcp/vm.py b/src/aigear/deploy/gcp/vm.py deleted file mode 100644 index a7a53b6..0000000 --- a/src/aigear/deploy/gcp/vm.py +++ /dev/null @@ -1,308 +0,0 @@ -# [START compute_instances_create] -from __future__ import annotations - -import re -import sys -from typing import Any -import warnings - -from google.api_core.extended_operation import ExtendedOperation -from collections.abc import Iterable -from google.cloud import compute_v1 - - -def get_image_from_family(project: str, family: str) -> compute_v1.Image: - """ - Retrieve the newest image that is part of a given family in a project. - - Args: - project: project ID or project number of the Cloud project you want to get image from. - family: name of the image family you want to get image from. - - Returns: - An Image object. - """ - image_client = compute_v1.ImagesClient() - # List of public operating system (OS) images: https://cloud.google.com/compute/docs/images/os-details - newest_image = image_client.get_from_family(project=project, family=family) - return newest_image - - -def disk_from_image( - disk_type: str, - disk_size_gb: int, - boot: bool, - source_image: str, - auto_delete: bool = True, -) -> compute_v1.AttachedDisk: - """ - Create an AttachedDisk object to be used in VM instance creation. Uses an image as the - source for the new disk. - - Args: - disk_type: the type of disk you want to create. This value uses the following format: - "zones/{zone}/diskTypes/(pd-standard|pd-ssd|pd-balanced|pd-extreme)". - For example: "zones/us-west3-b/diskTypes/pd-ssd" - disk_size_gb: size of the new disk in gigabytes - boot: boolean flag indicating whether this disk should be used as a boot disk of an instance - source_image: source image to use when creating this disk. You must have read access to this disk. This can be one - of the publicly available images or an image from one of your projects. - This value uses the following format: "projects/{project_name}/global/images/{image_name}" - auto_delete: boolean flag indicating whether this disk should be deleted with the VM that uses it - - Returns: - AttachedDisk object configured to be created using the specified image. - """ - boot_disk = compute_v1.AttachedDisk() - initialize_params = compute_v1.AttachedDiskInitializeParams() - initialize_params.source_image = source_image - initialize_params.disk_size_gb = disk_size_gb - initialize_params.disk_type = disk_type - boot_disk.initialize_params = initialize_params - # Remember to set auto_delete to True if you want the disk to be deleted when you delete - # your VM instance. - boot_disk.auto_delete = auto_delete - boot_disk.boot = boot - return boot_disk - - -def wait_for_extended_operation( - operation: ExtendedOperation, verbose_name: str = "operation", timeout: int = 300 -) -> Any: - """ - Waits for the extended (long-running) operation to complete. - - If the operation is successful, it will return its result. - If the operation ends with an error, an exception will be raised. - If there were any warnings during the execution of the operation - they will be printed to sys.stderr. - - Args: - operation: a long-running operation you want to wait on. - verbose_name: (optional) a more verbose name of the operation, - used only during error and warning reporting. - timeout: how long (in seconds) to wait for operation to finish. - If None, wait indefinitely. - - Returns: - Whatever the operation.result() returns. - - Raises: - This method will raise the exception received from `operation.exception()` - or RuntimeError if there is no exception set, but there is an `error_code` - set for the `operation`. - - In case of an operation taking longer than `timeout` seconds to complete, - a `concurrent.futures.TimeoutError` will be raised. - """ - result = operation.result(timeout=timeout) - - if operation.error_code: - print( - f"Error during {verbose_name}: [Code: {operation.error_code}]: {operation.error_message}", - file=sys.stderr, - flush=True, - ) - print(f"Operation ID: {operation.name}", file=sys.stderr, flush=True) - raise operation.exception() or RuntimeError(operation.error_message) - - if operation.warnings: - print(f"Warnings during {verbose_name}:\n", file=sys.stderr, flush=True) - for warning in operation.warnings: - print(f" - {warning.code}: {warning.message}", file=sys.stderr, flush=True) - - return result - - -def create_instance( - project_id: str, - zone: str, - instance_name: str, - disks: list[compute_v1.AttachedDisk], - machine_type: str = "n1-standard-1", - network_link: str = "global/networks/default", - subnetwork_link: str = None, - internal_ip: str = None, - external_access: bool = False, - external_ipv4: str = None, - accelerators: list[compute_v1.AcceleratorConfig] = None, - preemptible: bool = False, - spot: bool = False, - instance_termination_action: str = "STOP", - custom_hostname: str = None, - delete_protection: bool = False, -) -> compute_v1.Instance: - """ - Send an instance creation request to the Compute Engine API and wait for it to complete. - - Args: - project_id: project ID or project number of the Cloud project you want to use. - zone: name of the zone to create the instance in. For example: "us-west3-b" - instance_name: name of the new virtual machine (VM) instance. - disks: a list of compute_v1.AttachedDisk objects describing the disks - you want to attach to your new instance. - machine_type: machine type of the VM being created. This value uses the - following format: "zones/{zone}/machineTypes/{type_name}". - For example: "zones/europe-west3-c/machineTypes/f1-micro" - network_link: name of the network you want the new instance to use. - For example: "global/networks/default" represents the network - named "default", which is created automatically for each project. - subnetwork_link: name of the subnetwork you want the new instance to use. - This value uses the following format: - "regions/{region}/subnetworks/{subnetwork_name}" - internal_ip: internal IP address you want to assign to the new instance. - By default, a free address from the pool of available internal IP addresses of - used subnet will be used. - external_access: boolean flag indicating if the instance should have an external IPv4 - address assigned. - external_ipv4: external IPv4 address to be assigned to this instance. If you specify - an external IP address, it must live in the same region as the zone of the instance. - This setting requires `external_access` to be set to True to work. - accelerators: a list of AcceleratorConfig objects describing the accelerators that will - be attached to the new instance. - preemptible: boolean value indicating if the new instance should be preemptible - or not. Preemptible VMs have been deprecated and you should now use Spot VMs. - spot: boolean value indicating if the new instance should be a Spot VM or not. - instance_termination_action: What action should be taken once a Spot VM is terminated. - Possible values: "STOP", "DELETE" - custom_hostname: Custom hostname of the new VM instance. - Custom hostnames must conform to RFC 1035 requirements for valid hostnames. - delete_protection: boolean value indicating if the new virtual machine should be - protected against deletion or not. - Returns: - Instance object. - """ - instance_client = compute_v1.InstancesClient() - - # Use the network interface provided in the network_link argument. - network_interface = compute_v1.NetworkInterface() - network_interface.network = network_link - if subnetwork_link: - network_interface.subnetwork = subnetwork_link - - if internal_ip: - network_interface.network_i_p = internal_ip - - if external_access: - access = compute_v1.AccessConfig() - access.type_ = compute_v1.AccessConfig.Type.ONE_TO_ONE_NAT.name - access.name = "External NAT" - access.network_tier = access.NetworkTier.PREMIUM.name - if external_ipv4: - access.nat_i_p = external_ipv4 - network_interface.access_configs = [access] - - # Collect information into the Instance object. - instance = compute_v1.Instance() - instance.network_interfaces = [network_interface] - instance.name = instance_name - instance.disks = disks - if re.match(r"^zones/[a-z\d\-]+/machineTypes/[a-z\d\-]+$", machine_type): - instance.machine_type = machine_type - else: - instance.machine_type = f"zones/{zone}/machineTypes/{machine_type}" - - instance.scheduling = compute_v1.Scheduling() - if accelerators: - instance.guest_accelerators = accelerators - instance.scheduling.on_host_maintenance = ( - compute_v1.Scheduling.OnHostMaintenance.TERMINATE.name - ) - - if preemptible: - # Set the preemptible setting - warnings.warn( - "Preemptible VMs are being replaced by Spot VMs.", DeprecationWarning - ) - instance.scheduling = compute_v1.Scheduling() - instance.scheduling.preemptible = True - - if spot: - # Set the Spot VM setting - instance.scheduling.provisioning_model = ( - compute_v1.Scheduling.ProvisioningModel.SPOT.name - ) - instance.scheduling.instance_termination_action = instance_termination_action - - if custom_hostname is not None: - # Set the custom hostname for the instance - instance.hostname = custom_hostname - - if delete_protection: - # Set the delete protection bit - instance.deletion_protection = True - - # Prepare the request to insert an instance. - request = compute_v1.InsertInstanceRequest() - request.zone = zone - request.project = project_id - request.instance_resource = instance - - # Wait for the create operation to complete. - print(f"Creating the {instance_name} instance in {zone}...") - - operation = instance_client.insert(request=request) - - wait_for_extended_operation(operation, "instance creation") - - print(f"Instance {instance_name} created.") - return instance_client.get(project=project_id, zone=zone, instance=instance_name) - - -def list_instances(project_id: str, zone: str) -> Iterable[compute_v1.Instance]: - """ - List all instances in the given zone in the specified project. - - Args: - project_id: project ID or project number of the Cloud project you want to use. - zone: name of the zone you want to use. For example: “us-west3-b” - Returns: - An iterable collection of Instance objects. - """ - instance_client = compute_v1.InstancesClient() - instance_list = instance_client.list(project=project_id, zone=zone) - - print(f"Instances found in zone {zone}:") - for instance in instance_list: - print(f" - {instance.name} ({instance.machine_type})") - - -if __name__ == "__main__": - import google.auth - import google.auth.exceptions - - try: - default_project_id = google.auth.default()[1] - print(default_project_id) - except google.auth.exceptions.DefaultCredentialsError: - print( - "Please use `gcloud auth application-default login` " - "or set GOOGLE_APPLICATION_CREDENTIALS to use this script." - ) - else: - instance_name = "aigear-test-vm" - instance_zone = "asia-northeast1-a" - - newest_debian = get_image_from_family( - project="debian-cloud", family="debian-12" - ) - print(newest_debian.self_link) - disk_type = f"zones/{instance_zone}/diskTypes/pd-standard" - disks = [disk_from_image(disk_type, 10, True, newest_debian.self_link)] - machine_type = "n1-standard-1" - external_access = True - - # response = create_instance( - # default_project_id, - # instance_zone, - # instance_name, - # disks, - # machine_type, - # external_access=external_access - # ) - # print(response) - - list_instances(default_project_id, instance_zone) - -# gcloud compute ssh --project=ssc-ape-staging --zone=asia-northeast1-a aigear-test-vm -# gcloud compute instances create-with-container test-vm --container-image gcr.io/cloud-marketplace/google/nginx1:latest \ No newline at end of file From a43a9fcf1d1794ea7b20713bb3675dc6e740513a Mon Sep 17 00:00:00 2001 From: WangYunHua-TREC <10154231wang_yunhua@cn.tre-inc.com> Date: Thu, 29 Aug 2024 16:21:08 +0800 Subject: [PATCH 8/9] Add scheduler function(AIGEAR-50) --- src/aigear/deploy/gcp/images.py | 2 +- src/aigear/deploy/gcp/infra.py | 33 ++++---- src/aigear/deploy/gcp/project.py | 15 ++++ src/aigear/deploy/gcp/scheduler.py | 120 +++++++++++++++++++++++++++++ src/aigear/deploy/gcp/storage.py | 0 5 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 src/aigear/deploy/gcp/scheduler.py create mode 100644 src/aigear/deploy/gcp/storage.py diff --git a/src/aigear/deploy/gcp/images.py b/src/aigear/deploy/gcp/images.py index 3923088..62fd14c 100644 --- a/src/aigear/deploy/gcp/images.py +++ b/src/aigear/deploy/gcp/images.py @@ -70,7 +70,7 @@ def __init__( project_id: str, location: str, repository: str, - description: str, + description: str = "", ): super().__init__() self.project_id = project_id diff --git a/src/aigear/deploy/gcp/infra.py b/src/aigear/deploy/gcp/infra.py index 5a9df66..e9c055a 100644 --- a/src/aigear/deploy/gcp/infra.py +++ b/src/aigear/deploy/gcp/infra.py @@ -23,23 +23,9 @@ def __init__( _entry_point, topic_name, ) - self._check = self._check_infra(self.pub, self.sub, self.cf) - - @staticmethod - def _check_infra(pub, sub, cf): - pub_exist = pub.describe() - if not pub_exist: - logger.info("Pub(pubsub) not created.") - sub_exist = sub.describe() - if not sub_exist: - logger.info("Sub(pubsub) not created.") - cf_exist = cf.describe() - if not cf_exist: - logger.info("Cloud function not created.") - return pub_exist, sub_exist, cf_exist def create(self): - pub_exist, sub_exist, cf_exist = self._check + pub_exist, sub_exist, cf_exist = self._check_infra(self.pub, self.sub, self.cf) if not pub_exist: self.pub.create() @@ -48,8 +34,8 @@ def create(self): if not cf_exist: self.cf.deploy() - - def delete(self): + + def clear(self): project_id = get_project_id() owner_pm = check_iam(project_id) if owner_pm: @@ -58,3 +44,16 @@ def delete(self): self.cf.delete() else: logger.info("You are not the owner and cannot delete aigear infrastructure.") + + @staticmethod + def _check_infra(pub, sub, cf): + pub_exist = pub.describe() + if not pub_exist: + logger.info("Pub(pubsub) not created.") + sub_exist = sub.describe() + if not sub_exist: + logger.info("Sub(pubsub) not created.") + cf_exist = cf.describe() + if not cf_exist: + logger.info("Cloud function not created.") + return pub_exist, sub_exist, cf_exist diff --git a/src/aigear/deploy/gcp/project.py b/src/aigear/deploy/gcp/project.py index 2d0a125..ca12ad0 100644 --- a/src/aigear/deploy/gcp/project.py +++ b/src/aigear/deploy/gcp/project.py @@ -15,3 +15,18 @@ def get_project_id(): else: project_id = event.strip() return project_id + + +def get_region(): + region = None + command = [ + "gcloud", "config", "get-value", "compute/region" + ] + event = run_sh(command) + if "unset" in event: + logger.info("No project id set.") + elif "ERROR" in event: + logger.info(event) + else: + region = event.strip() + return region diff --git a/src/aigear/deploy/gcp/scheduler.py b/src/aigear/deploy/gcp/scheduler.py new file mode 100644 index 0000000..dddfe67 --- /dev/null +++ b/src/aigear/deploy/gcp/scheduler.py @@ -0,0 +1,120 @@ +import json +from ...common.logger import logger +from ...common.sh import run_sh + + +class Scheduler: + def __init__( + self, + name: str, + location: str, + schedule: str, + topic_name: str, + message: any, + time_zone: str = "Etc/UTC", + ): + self.name = name + self.location = location + self.schedule = schedule + self.topic_name = topic_name + self.message = message + self.time_zone = time_zone + + def create(self): + message_body = json.dumps(self.message) + command = [ + "gcloud", "scheduler", "jobs", "create", "pubsub", + self.name, + "--location", self.location, + "--schedule", self.schedule, + "--topic", self.topic_name, + "--message-body", message_body, + "--time-zone", self.time_zone, + ] + event = run_sh(command) + logger.info(event) + if "ERROR" in event: + logger.info("Error occurred while creating cloud function.") + + def delete(self): + command = [ + "gcloud", "scheduler", "jobs", "delete", + self.name, + "--location", self.location, + ] + event = run_sh(command, "yes\n") + logger.info(event) + + def describe(self): + is_exist = False + command = [ + "gcloud", "scheduler", "jobs", "describe", + self.name, + "--location", self.location, + ] + event = run_sh(command) + logger.info(event) + if "ENABLED" in event: + is_exist = True + return is_exist + + def list(self): + command = [ + "gcloud", "scheduler", "jobs", "list", + "--location", self.location, + f"--filter={self.name}", + ] + event = run_sh(command) + logger.info(f"\n{event}") + + def run(self): + command = [ + "gcloud", "scheduler", "jobs", "run", + self.name, + "--location", self.location, + ] + event = run_sh(command) + if event: + logger.info(event) + else: + logger.info("Running successfully, executing job.") + + def pause(self): + command = [ + "gcloud", "scheduler", "jobs", "pause", + self.name, + "--location", self.location, + ] + event = run_sh(command) + logger.info(event) + + def resume(self): + command = [ + "gcloud", "scheduler", "jobs", "resume", + self.name, + "--location", self.location, + ] + event = run_sh(command) + logger.info(event) + + @staticmethod + def update( + name, + location, + schedule, + topic_name, + message, + ): + message_body = json.dumps(message) + command = [ + "gcloud", "scheduler", "jobs", "update", "pubsub", + name, + "--location", location, + "--schedule", schedule, + "--topic", topic_name, + "--message-body", message_body, + ] + event = run_sh(command) + logger.info(event) + if "ERROR" in event: + logger.info("Error occurred while creating cloud function.") diff --git a/src/aigear/deploy/gcp/storage.py b/src/aigear/deploy/gcp/storage.py new file mode 100644 index 0000000..e69de29 From 2e8ce220c76cb09fddbb7f2f5543eba41e1f8d20 Mon Sep 17 00:00:00 2001 From: WangYunHua-TREC <10154231wang_yunhua@cn.tre-inc.com> Date: Fri, 30 Aug 2024 15:55:41 +0800 Subject: [PATCH 9/9] Add the bucket(AIGEAR-46) --- src/aigear/deploy/gcp/scheduler.py | 32 +++++----- src/aigear/deploy/gcp/storage.py | 95 ++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 14 deletions(-) diff --git a/src/aigear/deploy/gcp/scheduler.py b/src/aigear/deploy/gcp/scheduler.py index dddfe67..f51c675 100644 --- a/src/aigear/deploy/gcp/scheduler.py +++ b/src/aigear/deploy/gcp/scheduler.py @@ -21,20 +21,24 @@ def __init__( self.time_zone = time_zone def create(self): - message_body = json.dumps(self.message) - command = [ - "gcloud", "scheduler", "jobs", "create", "pubsub", - self.name, - "--location", self.location, - "--schedule", self.schedule, - "--topic", self.topic_name, - "--message-body", message_body, - "--time-zone", self.time_zone, - ] - event = run_sh(command) - logger.info(event) - if "ERROR" in event: - logger.info("Error occurred while creating cloud function.") + is_exist = self.describe() + if not is_exist: + message_body = json.dumps(self.message) + command = [ + "gcloud", "scheduler", "jobs", "create", "pubsub", + self.name, + "--location", self.location, + "--schedule", self.schedule, + "--topic", self.topic_name, + "--message-body", message_body, + "--time-zone", self.time_zone, + ] + event = run_sh(command) + logger.info(event) + if "ERROR" in event: + logger.info("Error occurred while creating cloud function.") + else: + logger.info(f"the cloud scheduler(self.name) already exists.") def delete(self): command = [ diff --git a/src/aigear/deploy/gcp/storage.py b/src/aigear/deploy/gcp/storage.py index e69de29..a2abe30 100644 --- a/src/aigear/deploy/gcp/storage.py +++ b/src/aigear/deploy/gcp/storage.py @@ -0,0 +1,95 @@ +from ...common.logger import logger +from ...common.sh import run_sh + + +class Bucket: + def __init__( + self, + bucket_name: str, + project_id: str, + location: str, + ): + self.bucket = f"gs://{bucket_name}-{project_id}" + self.location = location + + def create(self): + command = [ + "gcloud", "storage", "buckets", "create", + self.bucket, + f"--location={self.location}", + "--uniform-bucket-level-access", + ] + event = run_sh(command) + logger.info(event) + + def describe(self): + is_exist = False + command = [ + "gcloud", "storage", "buckets", "describe", + self.bucket, + ] + event = run_sh(command) + logger.info(event) + if self.bucket in event and "ERROR" not in event: + is_exist = True + return is_exist + + def list(self): + command = [ + "gcloud", "storage", "buckets", "list", + self.bucket, + ] + event = run_sh(command) + logger.info(f"\n{event}") + + def delete(self): + command = [ + "gcloud", "storage", "rm", "-r", + self.bucket, + ] + event = run_sh(command) + logger.info(event) + + +class ManagedFolders: + def __init__(self, bucket_name, project_id): + self.bucket = f"gs://{bucket_name}-{project_id}" + + def create(self, folder_name): + folder = f"{self.bucket}/{folder_name}" + command = [ + "gcloud", "storage", "managed-folders", "create", + folder, + ] + event = run_sh(command) + logger.info(event) + + def describe(self, folder_name): + is_exist = False + folder = f"{self.bucket}/{folder_name}" + command = [ + "gcloud", "storage", "managed-folders", "describe", + folder, + ] + event = run_sh(command) + logger.info(event) + if folder in event and "ERROR" not in event: + is_exist = True + return is_exist + + def list(self): + command = [ + "gcloud", "storage", "managed-folders", "list", + self.bucket, + ] + event = run_sh(command) + logger.info(f"\n{event}") + + def delete(self, folder_name): + folder = f"{self.bucket}/{folder_name}" + command = [ + "gcloud", "storage", "managed-folders", "delete", + folder, + ] + event = run_sh(command) + logger.info(event)