diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ad0c22faaf1..9d7d355d5b5 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -50,8 +50,4 @@ first_value = 1 [bumpversion:file:packages/grid/helm/syft/values.yaml] -[bumpversion:file:packages/hagrid/hagrid/manifest_template.yml] - -[bumpversion:file:packages/hagrid/hagrid/deps.py] - [bumpversion:file:packages/syftcli/manifest.yml] diff --git a/.bumpversion_stable.cfg b/.bumpversion_stable.cfg index 806698f59f1..52b011ac7b1 100644 --- a/.bumpversion_stable.cfg +++ b/.bumpversion_stable.cfg @@ -13,7 +13,3 @@ serialize = {major}.{minor}.{patch} [bumpversion:file:packages/syft/src/syft/stable_version.py] - -[bumpversion:file:packages/hagrid/hagrid/stable_version.py] - -[bumpversion:file:packages/hagrid/hagrid/cache.py] diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 36bccce0507..00000000000 --- a/.dockerignore +++ /dev/null @@ -1,11 +0,0 @@ -.tox -.git -.vscode -scripts -.mypy_cache -.benchmarks -docker -packages/syft/src/target -packages/grid/apps/domain/src/nodedatabase.db -packages/grid/apps/network/src/nodedatabase.db -packages/grid/apps/worker/src/nodedatabase.db diff --git a/.github/file-filters.yml b/.github/file-filters.yml index 1d0a44134cc..be000a84640 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -27,17 +27,6 @@ backend: - packages/grid/backend/**/*.sh - packages/grid/backend/**/*.mako -hagrid: - - .github/workflows/pr-tests-hagrid.yml - - packages/hagrid/**/*.py - - packages/hagrid/**/*.cfg - - packages/hagrid/**/*.yml - - packages/hagrid/**/*.dockerfile - - packages/hagrid/**/*.toml - - packages/hagrid/**/*.txt - - packages/hagrid/**/*.ini - - packages/hagrid/**/*.sh - syft: - .github/workflows/pr-tests-syft.yml - packages/syft/**/*.py diff --git a/.github/workflows/cd-hagrid.yml b/.github/workflows/cd-hagrid.yml deleted file mode 100644 index a17f61ec519..00000000000 --- a/.github/workflows/cd-hagrid.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: CD - HAGrid - -on: - schedule: - - cron: "00 10 * * */3" # At 10:00 UTC on every three days - - workflow_dispatch: - inputs: - skip_tests: - description: "If true, skip tests" - required: false - default: "false" - -# Prevents concurrent runs of the same workflow -# while the previous run is still in progress -concurrency: - group: "CD - Hagrid" - cancel-in-progress: false - -jobs: - call-pr-tests-linting: - if: github.repository == 'OpenMined/PySyft' && (github.event.inputs.skip_tests == 'false' || github.event_name == 'schedule') # don't run on forks - uses: OpenMined/PySyft/.github/workflows/pr-tests-linting.yml@dev - - call-pr-tests-syft: - if: github.repository == 'OpenMined/PySyft' && (github.event.inputs.skip_tests == 'false' || github.event_name == 'schedule') # don't run on forks - uses: OpenMined/PySyft/.github/workflows/pr-tests-syft.yml@dev - - call-pr-tests-stack: - if: github.repository == 'OpenMined/PySyft' && (github.event.inputs.skip_tests == 'false' || github.event_name == 'schedule') # don't run on forks - uses: OpenMined/PySyft/.github/workflows/pr-tests-stack.yml@dev - secrets: inherit - - call-hagrid-tests: - if: github.repository == 'OpenMined/PySyft' && (github.event.inputs.skip_tests == 'false' || github.event_name == 'schedule') # don't run on forks - uses: OpenMined/PySyft/.github/workflows/pr-tests-hagrid.yml@dev - - deploy-hagrid: - needs: - [ - call-pr-tests-linting, - call-pr-tests-syft, - call-pr-tests-stack, - call-hagrid-tests, - ] - if: always() && (needs.call-pr-tests-linting.result == 'success' && needs.call-pr-tests-syft.result == 'success' && needs.call-pr-tests-stack.result == 'success' && needs.call-hagrid-tests.result == 'success' || github.event.inputs.skip_tests == 'true') - runs-on: ubuntu-latest - - outputs: - current_hash: ${{ steps.get_hash.outputs.current_hash }} - previous_hash: ${{ steps.get_hash.outputs.previous_hash }} - steps: - - uses: actions/checkout@v4 - with: - token: ${{ secrets.SYFT_BOT_COMMIT_TOKEN }} - - name: Install checksumdir - run: | - pip install --upgrade checksumdir - - name: Get the hashes - id: get-hashes - shell: bash - run: | - current_hash=$(checksumdir ./packages/hagrid) - echo "current_hash=$current_hash" >> $GITHUB_OUTPUT - previous_hash=$(cat ./scripts/hagrid_hash) - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install dependencies - if: ${{needs.hagrid-deploy.outputs.current_hash}} != ${{needs.hagrid-deploy.outputs.previous_hash}} - run: | - python -m pip install --upgrade pip - pip install --upgrade tox setuptools wheel twine bump2version PyYAML - - - name: Bump the Version - if: ${{needs.hagrid-deploy.outputs.current_hash}} != ${{needs.hagrid-deploy.outputs.previous_hash}} - run: | - python3 hagrid/version.py - python3 scripts/update_manifest.py - bump2version patch --allow-dirty --no-commit - tox -e lint || true - python3 hagrid/version.py - working-directory: ./packages/hagrid - - - name: Write the new hash - if: ${{needs.hagrid-deploy.outputs.current_hash}} != ${{needs.hagrid-deploy.outputs.previous_hash}} - run: echo $(checksumdir packages/hagrid) > ./scripts/hagrid_hash - - - name: Commit changes - if: ${{needs.hagrid-deploy.outputs.current_hash}} != ${{needs.hagrid-deploy.outputs.previous_hash}} - uses: EndBug/add-and-commit@v9 - with: - author_name: ${{ secrets.OM_BOT_NAME }} - author_email: ${{ secrets.OM_BOT_EMAIL }} - message: "[hagrid] bump version" - add: "['./packages/hagrid/.bumpversion.cfg','./packages/hagrid/setup.py','./packages/hagrid/hagrid/version.py', './scripts/hagrid_hash', './packages/hagrid/hagrid/manifest_template.yml']" - - - name: Build and publish - if: ${{needs.hagrid-deploy.outputs.current_hash}} != ${{needs.hagrid-deploy.outputs.previous_hash}} - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.HAGRID_BUMP_TOKEN }} - run: | - tox -e hagrid.publish - twine upload packages/hagrid/dist/* diff --git a/.github/workflows/cd-post-release-tests.yml b/.github/workflows/cd-post-release-tests.yml index 36bd38c6131..370469ea0bb 100644 --- a/.github/workflows/cd-post-release-tests.yml +++ b/.github/workflows/cd-post-release-tests.yml @@ -30,105 +30,6 @@ on: default: "REAL_PYPI" jobs: - notebook-test-hagrid: - if: github.event.inputs.release_platform == 'REAL_PYPI' - strategy: - max-parallel: 99 - matrix: - os: [ubuntu-latest] - python-version: ["3.12"] - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - # free 10GB of space - - name: Remove unnecessary files - if: matrix.os == 'ubuntu-latest' - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - docker image prune --all --force - docker builder prune --all --force - docker system prune --all --force - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - - with: - python-version: ${{ matrix.python-version }} - - - name: Upgrade pip - run: | - python -m pip install --upgrade --user pip - - - name: Get pip cache dir - id: pip-cache - shell: bash - run: | - echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - - - name: pip cache - uses: actions/cache@v4 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-py${{ matrix.python-version }}-${{ hashFiles('setup.cfg') }} - restore-keys: | - ${{ runner.os }}-pip-py${{ matrix.python-version }}- - - - name: Install Hagrid, tox and uv - run: | - pip install -U hagrid - pip install --upgrade pip uv==0.1.35 tox tox-uv==1.5.1 - - - name: Hagrid Version - run: | - hagrid version - - - name: Remove existing containers - continue-on-error: true - shell: bash - run: | - docker rm $(docker ps -aq) --force || true - docker volume prune -f || true - docker buildx use default || true - - - name: Launch Domain - run: | - hagrid launch test-domain-1 to docker:8081 --tag=${{ inputs.syft_version }} --low-side - - - name: Run tests - env: - NODE_PORT: "8081" - SYFT_VERSION: ${{ inputs.syft_version }} - EXCLUDE_NOTEBOOKS: "not 11-container-images-k8s.ipynb" - run: | - tox -e e2e.test.notebook - - #Run log collector python script - - name: Run log collector - timeout-minutes: 5 - if: failure() - shell: bash - run: | - python ./scripts/container_log_collector.py - - # Get Job name and url - - name: Get job name and url - id: job_name - if: failure() - shell: bash - run: | - echo "job_name=$(echo ${{ github.job }})" >> $GITHUB_OUTPUT - echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - - - name: Upload logs to GitHub - uses: actions/upload-artifact@master - if: failure() - with: - name: ${{ matrix.os }}-${{ steps.job_name.outputs.job_name }}-logs-${{ steps.job_name.outputs.date }} - path: ./logs/${{ steps.job_name.outputs.job_name}}/ - syft-install-check: strategy: max-parallel: 99 @@ -290,9 +191,8 @@ jobs: pip install syft[data_science,dev]==${{ inputs.syft_version }} fi - - name: Install Hagrid, tox and uv + - name: Install tox and uv run: | - pip install -U hagrid pip install --upgrade pip uv==0.1.35 tox tox-uv==1.5.1 tox-current-env - name: Run unit tests diff --git a/.github/workflows/cd-syft.yml b/.github/workflows/cd-syft.yml index b842cd0d84e..c153465dd6a 100644 --- a/.github/workflows/cd-syft.yml +++ b/.github/workflows/cd-syft.yml @@ -396,7 +396,6 @@ jobs: bump2version prenum --allow-dirty --no-commit ls **/VERSION | xargs -I {} python {} cat packages/grid/devspace.yaml | grep '0\.' - python packages/hagrid/scripts/update_manifest.py $(python packages/grid/VERSION) - name: Generate Release Metadata id: release_checks @@ -460,7 +459,7 @@ jobs: author_name: ${{ secrets.OM_BOT_NAME }} author_email: ${{ secrets.OM_BOT_EMAIL }} message: "[syft]bump version" - add: "['.bumpversion.cfg', 'VERSION', 'packages/grid/VERSION','packages/syft/PYPI.md', 'packages/grid/devspace.yaml', 'packages/syft/src/syft/VERSION', 'packages/syft/setup.cfg', 'packages/grid/frontend/package.json', 'packages/syft/src/syft/__init__.py', 'packages/hagrid/hagrid/manifest_template.yml', 'packages/grid/helm/syft/Chart.yaml','packages/grid/helm/repo', 'packages/hagrid/hagrid/deps.py', 'packages/grid/podman/podman-kube/podman-syft-kube.yaml' ,'packages/grid/podman/podman-kube/podman-syft-kube-config.yaml', 'packages/syftcli/manifest.yml', 'packages/syft/src/syft/protocol/protocol_version.json', 'packages/syft/src/syft/protocol/releases/', 'packages/grid/backend/grid/images/worker_cpu.dockerfile','packages/grid/helm/syft/values.yaml','packages/grid/helm/syft']" + add: "['.bumpversion.cfg', 'VERSION', 'packages/grid/VERSION','packages/syft/PYPI.md', 'packages/grid/devspace.yaml', 'packages/syft/src/syft/VERSION', 'packages/syft/setup.cfg', 'packages/grid/frontend/package.json', 'packages/syft/src/syft/__init__.py', 'packages/grid/helm/syft/Chart.yaml','packages/grid/helm/repo', 'packages/grid/podman/podman-kube/podman-syft-kube.yaml' ,'packages/grid/podman/podman-kube/podman-syft-kube-config.yaml', 'packages/syftcli/manifest.yml', 'packages/syft/src/syft/protocol/protocol_version.json', 'packages/syft/src/syft/protocol/releases/', 'packages/grid/backend/grid/images/worker_cpu.dockerfile','packages/grid/helm/syft/values.yaml','packages/grid/helm/syft']" - name: Changes to commit to Syft Repo during stable release if: needs.merge-docker-images.outputs.release_tag == 'latest' @@ -539,7 +538,6 @@ jobs: files: | ./packages/syftcli/manifest.yml ./build/syftcli-config/* - ./packages/hagrid/hagrid/manifest_template.yml tag_name: v${{ steps.release_checks.outputs.github_release_version }} # Checkout to gh-pages and update helm repo diff --git a/.github/workflows/container-scan.yml b/.github/workflows/container-scan.yml index f7b5df009ae..303eb11bc40 100644 --- a/.github/workflows/container-scan.yml +++ b/.github/workflows/container-scan.yml @@ -274,30 +274,30 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Snyk CLI to check for security issues - # Snyk can be used to break the build when it detects security issues. - # In this case we want to upload the SAST issues to GitHub Code Scanning - uses: snyk/actions/setup@master - env: - # This is where you will need to introduce the Snyk API token created with your Snyk account - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - - name: Snyk auth - shell: bash - run: snyk config set api=$SNYK_TOKEN - env: - # This is where you will need to introduce the Snyk API token created with your Snyk account - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - name: Snyk Container test + uses: snyk/actions/docker@master continue-on-error: true - shell: bash - run: snyk container test mongo:7.0.0 --sarif --sarif-file-output=snyk-code.sarif env: # This is where you will need to introduce the Snyk API token created with your Snyk account SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + image: mongo:7.0.0 + args: --sarif-file-output=snyk-code.sarif + + # Replace any "undefined" security severity values with 0. The undefined value is used in the case + # of license-related findings, which do not do not indicate a security vulnerability. + # See https://github.com/github/codeql-action/issues/2187 for more context. + - name: Post-process sarif output + run: | + sed -i 's/"security-severity": "undefined"/"security-severity": "0"/g' snyk-code.sarif + + # Replace any "null" security severity values with 0. The undefined value is used in the case + # the NVD CVSS Score is not available. + # See https://github.com/Erikvl87/docker-languagetool/issues/90 and https://github.com/github/codeql-action/issues/2187 for more context. + - name: Post-process sarif output for security severities set to "null" + run: | + sed -i 's/"security-severity": "null"/"security-severity": "0"/g' snyk-code.sarif - # Push the Snyk Code results into GitHub Code Scanning tab - name: Upload result to GitHub Code Scanning uses: github/codeql-action/upload-sarif@v3 with: @@ -352,29 +352,29 @@ jobs: actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Snyk CLI to check for security issues - # Snyk can be used to break the build when it detects security issues. - # In this case we want to upload the SAST issues to GitHub Code Scanning - uses: snyk/actions/setup@master - env: - # This is where you will need to introduce the Snyk API token created with your Snyk account - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - - name: Snyk auth - shell: bash - run: snyk config set api=$SNYK_TOKEN - env: - # This is where you will need to introduce the Snyk API token created with your Snyk account - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - name: Snyk Container test + uses: snyk/actions/docker@master continue-on-error: true - shell: bash - run: snyk container test traefik:v2.11.0 --sarif --sarif-file-output=snyk-code.sarif env: # This is where you will need to introduce the Snyk API token created with your Snyk account SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + image: traefik:v2.11.0 + args: --sarif-file-output=snyk-code.sarif + + # Replace any "undefined" security severity values with 0. The undefined value is used in the case + # of license-related findings, which do not do not indicate a security vulnerability. + # See https://github.com/github/codeql-action/issues/2187 for more context. + - name: Post-process sarif output + run: | + sed -i 's/"security-severity": "undefined"/"security-severity": "0"/g' snyk-code.sarif + + # Replace any "null" security severity values with 0. The undefined value is used in the case + # the NVD CVSS Score is not available. + # See https://github.com/Erikvl87/docker-languagetool/issues/90 and https://github.com/github/codeql-action/issues/2187 for more context. + - name: Post-process sarif output for security severities set to "null" + run: | + sed -i 's/"security-severity": "null"/"security-severity": "0"/g' snyk-code.sarif # Push the Snyk Code results into GitHub Code Scanning tab - name: Upload result to GitHub Code Scanning diff --git a/.github/workflows/e2e-tests-notebook.yml b/.github/workflows/e2e-tests-notebook.yml index a7fe68cee6b..10c3eb84e2d 100644 --- a/.github/workflows/e2e-tests-notebook.yml +++ b/.github/workflows/e2e-tests-notebook.yml @@ -40,7 +40,7 @@ on: type: string jobs: - notebook-test-hagrid: + notebook-test-e2e: strategy: max-parallel: 99 matrix: diff --git a/.github/workflows/manual-delete-buildjet-cache.yml b/.github/workflows/manual-delete-buildjet-cache.yml deleted file mode 100644 index 97370c02406..00000000000 --- a/.github/workflows/manual-delete-buildjet-cache.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Manually Delete BuildJet Cache -on: - workflow_dispatch: - inputs: - cache_key: - description: "BuildJet Cache Key to Delete" - required: true - type: string -jobs: - manually-delete-buildjet-cache: - strategy: - matrix: - os: [ubuntu-latest] - python-version: ["3.10", "3.11", "3.12"] - - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - uses: buildjet/cache-delete@v1 - with: - cache_key: ${{ inputs.cache_key }} diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 6db4d8df53c..491b4dd6aad 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -14,10 +14,6 @@ jobs: if: github.repository == 'OpenMined/PySyft' # don't run on forks uses: OpenMined/PySyft/.github/workflows/pr-tests-linting.yml@dev - call-pr-tests-hagrid: - if: github.repository == 'OpenMined/PySyft' # don't run on forks - uses: OpenMined/PySyft/.github/workflows/pr-tests-hagrid.yml@dev - call-pr-tests-syft: if: github.repository == 'OpenMined/PySyft' # don't run on forks uses: OpenMined/PySyft/.github/workflows/pr-tests-syft.yml@dev diff --git a/.github/workflows/pr-tests-frontend.yml b/.github/workflows/pr-tests-frontend.yml index 7d669b61da8..bf36991a385 100644 --- a/.github/workflows/pr-tests-frontend.yml +++ b/.github/workflows/pr-tests-frontend.yml @@ -61,13 +61,13 @@ jobs: if: steps.changes.outputs.frontend == 'true' with: path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-uv-py${{ matrix.python-version }}-${{ hashFiles('packages/hagrid/setup.cfg') }} + key: ${{ runner.os }}-uv-py${{ matrix.python-version }}-frontend restore-keys: | ${{ runner.os }}-uv-py${{ matrix.python-version }}- - name: Docker on MacOS if: steps.changes.outputs.frontend == 'true' && matrix.os == 'macos-latest' - uses: crazy-max/ghaction-setup-docker@v3.1.0 + uses: crazy-max/ghaction-setup-docker@v3.2.0 - name: Install Tox if: steps.changes.outputs.frontend == 'true' diff --git a/.github/workflows/pr-tests-hagrid.yml b/.github/workflows/pr-tests-hagrid.yml deleted file mode 100644 index a8e0e30e93f..00000000000 --- a/.github/workflows/pr-tests-hagrid.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: PR Tests - HAGrid - -on: - workflow_call: - - pull_request: - branches: - - dev - - main - - "0.8" - -concurrency: - group: hagrid-${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || github.workflow_ref }} - cancel-in-progress: true - -defaults: - run: - working-directory: ./packages/hagrid - -jobs: - pr-tests-syft-hagrid-comptability: - strategy: - max-parallel: 99 - matrix: - os: [ubuntu-latest] - python-version: ["3.11"] - syft-version: ["0.8.2", "0.8.2b6", "0.8.3"] - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - name: Check for file changes - uses: dorny/paths-filter@v3 - id: changes - with: - base: ${{ github.ref }} - token: ${{ github.token }} - filters: .github/file-filters.yml - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - if: steps.changes.outputs.hagrid == 'true' - with: - python-version: ${{ matrix.python-version }} - - - name: Upgrade pip - if: steps.changes.outputs.hagrid == 'true' - run: | - python -m pip install --upgrade --user pip - - - name: Get pip cache dir - id: pip-cache - if: steps.changes.outputs.hagrid == 'true' - shell: bash - run: | - echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - - - name: pip cache - uses: actions/cache@v4 - if: steps.changes.outputs.hagrid == 'true' - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-py${{ matrix.python-version }}-${{ hashFiles('packages/syft/setup.cfg') }} - restore-keys: | - ${{ runner.os }}-pip-py${{ matrix.python-version }}-${{ hashFiles('packages/syft/setup.cfg') }} - - # https://github.com/google/jax/issues/17693 - # pinning ml-dtypes due to jax version==0.4.10 - - name: Install Syft ${{ matrix.syft-version }} - if: steps.changes.outputs.hagrid == 'true' - run: | - pip install ml-dtypes==0.2.0 - pip install syft==${{ matrix.syft-version }} - pip install . - - - name: Run Orchestra Command - if: steps.changes.outputs.hagrid == 'true' - run: | - python -c "import syft as sy; domain1 = sy.orchestra.launch(name='test-domain-1', dev_mode=True, reset=True)" - python -c "import syft as sy; domain2 = sy.orchestra.launch(name='test-domain-2',dev_mode=False, reset=True)" diff --git a/.github/workflows/pr-tests-stack-arm64.yml b/.github/workflows/pr-tests-stack-arm64.yml index 6dc275c8f6b..7bb4e07de40 100644 --- a/.github/workflows/pr-tests-stack-arm64.yml +++ b/.github/workflows/pr-tests-stack-arm64.yml @@ -1,102 +1,104 @@ -name: PR Tests - Stack - Arm64 - -on: - workflow_call: - - workflow_dispatch: - inputs: - none: - description: "Run Version Tests Manually" - required: false - -concurrency: - group: stackarm64-${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || github.workflow_ref }} - cancel-in-progress: true - -jobs: - pr-tests-stack-arm64: - strategy: - max-parallel: 3 - matrix: - os: [ubuntu-latest] - python-version: ["3.12"] - - runs-on: ${{matrix.os}} - - steps: - # - name: set permissions on work folder for self-runners - # run: | - # sudo chown -R $USER:$USER ~/actions-runner/_work/ - - - uses: actions/checkout@v4 - - # free 10GB of space - - name: Remove unnecessary files - if: matrix.os == 'ubuntu-latest' - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - docker image prune --all --force - docker builder prune --all --force - docker system prune --all --force - - - name: Check for file changes - uses: dorny/paths-filter@v3 - id: changes - with: - base: ${{ github.ref }} - token: ${{ github.token }} - filters: .github/file-filters.yml - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Upgrade pip - run: | - pip install --upgrade pip uv==0.1.35 - uv --version - - # - name: Get pip cache dir - # id: pip-cache - # shell: bash - # run: | - # echo "dir=$(uv cache dir)" >> $GITHUB_OUTPUT - - # - name: pip cache - # uses: actions/cache@v3 - # with: - # path: ${{ steps.pip-cache.outputs.dir }} - # key: ${{ runner.os }}-uv-py${{ matrix.python-version }} - # restore-keys: | - # ${{ runner.os }}-uv-py${{ matrix.python-version }} - - - name: Install tox - run: | - pip install --upgrade tox tox-uv==1.5.1 - - - name: Install Docker Compose - if: runner.os == 'Linux' - shell: bash - run: | - mkdir -p ~/.docker/cli-plugins - DOCKER_COMPOSE_VERSION=v2.21.0 - curl -sSL https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose - chmod +x ~/.docker/cli-plugins/docker-compose - - - name: Setup linux/arm64 Docker - run: | - docker rm $(docker ps -aq) --force || true - docker volume prune -f || true - docker buildx create --platform linux/arm64 --name arm64builder || true - docker buildx use arm64builder || true - docker run --privileged --rm tonistiigi/binfmt --install arm64 - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - - - name: Run integration tests - uses: nick-fields/retry@v3 - with: - timeout_seconds: 36000 - max_attempts: 3 - command: EMULATION="true" HAGRID_FLAGS="--tag=local --test --platform linux/arm64" tox -e stack.test.integration +# The following test is disable temporarily until we switch to +# Self hosted runners for running the arm64 tests +# name: PR Tests - Stack - Arm64 + +# on: +# workflow_call: + +# workflow_dispatch: +# inputs: +# none: +# description: "Run Version Tests Manually" +# required: false + +# concurrency: +# group: stackarm64-${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || github.workflow_ref }} +# cancel-in-progress: true + +# jobs: +# pr-tests-stack-arm64: +# strategy: +# max-parallel: 3 +# matrix: +# os: [ubuntu-latest] +# python-version: ["3.12"] + +# runs-on: ${{matrix.os}} + +# steps: +# # - name: set permissions on work folder for self-runners +# # run: | +# # sudo chown -R $USER:$USER ~/actions-runner/_work/ + +# - uses: actions/checkout@v4 + +# # free 10GB of space +# - name: Remove unnecessary files +# if: matrix.os == 'ubuntu-latest' +# run: | +# sudo rm -rf /usr/share/dotnet +# sudo rm -rf "$AGENT_TOOLSDIRECTORY" +# docker image prune --all --force +# docker builder prune --all --force +# docker system prune --all --force + +# - name: Check for file changes +# uses: dorny/paths-filter@v3 +# id: changes +# with: +# base: ${{ github.ref }} +# token: ${{ github.token }} +# filters: .github/file-filters.yml + +# - name: Set up Python ${{ matrix.python-version }} +# uses: actions/setup-python@v5 +# with: +# python-version: ${{ matrix.python-version }} + +# - name: Upgrade pip +# run: | +# pip install --upgrade pip uv==0.1.35 +# uv --version + +# # - name: Get pip cache dir +# # id: pip-cache +# # shell: bash +# # run: | +# # echo "dir=$(uv cache dir)" >> $GITHUB_OUTPUT + +# # - name: pip cache +# # uses: actions/cache@v3 +# # with: +# # path: ${{ steps.pip-cache.outputs.dir }} +# # key: ${{ runner.os }}-uv-py${{ matrix.python-version }} +# # restore-keys: | +# # ${{ runner.os }}-uv-py${{ matrix.python-version }} + +# - name: Install tox +# run: | +# pip install --upgrade tox tox-uv==1.5.1 + +# - name: Install Docker Compose +# if: runner.os == 'Linux' +# shell: bash +# run: | +# mkdir -p ~/.docker/cli-plugins +# DOCKER_COMPOSE_VERSION=v2.21.0 +# curl -sSL https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose +# chmod +x ~/.docker/cli-plugins/docker-compose + +# - name: Setup linux/arm64 Docker +# run: | +# docker rm $(docker ps -aq) --force || true +# docker volume prune -f || true +# docker buildx create --platform linux/arm64 --name arm64builder || true +# docker buildx use arm64builder || true +# docker run --privileged --rm tonistiigi/binfmt --install arm64 +# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + +# - name: Run integration tests +# uses: nick-fields/retry@v3 +# with: +# timeout_seconds: 36000 +# max_attempts: 3 +# command: tox -e stack.test.integration diff --git a/.github/workflows/pr-tests-stack-public.yml b/.github/workflows/pr-tests-stack-public.yml deleted file mode 100644 index a036d7b5e07..00000000000 --- a/.github/workflows/pr-tests-stack-public.yml +++ /dev/null @@ -1,216 +0,0 @@ -name: PR Tests - Stack - Public - -on: - workflow_call: - - workflow_dispatch: - inputs: - none: - description: "Run Stack Integration Tests Manually" - required: false - -concurrency: - group: stackpublic-${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || github.workflow_ref }} - cancel-in-progress: true - -jobs: - pr-tests-stack-public: - strategy: - max-parallel: 99 - matrix: - os: [ubuntu-latest, macos-latest, windows] - python-version: ["3.12"] - pytest-modules: ["frontend network"] - fail-fast: false - - runs-on: ${{matrix.os}} - - steps: - - name: "clean .git/config" - if: matrix.os == 'windows' - continue-on-error: true - shell: bash - run: | - echo "deleting ${GITHUB_WORKSPACE}/.git/config" - rm ${GITHUB_WORKSPACE}/.git/config - - - uses: actions/checkout@v4 - - - name: Check for file changes - uses: dorny/paths-filter@v3 - id: changes - with: - base: ${{ github.ref }} - token: ${{ github.token }} - filters: .github/file-filters.yml - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - if: steps.changes.outputs.stack == 'true' - with: - python-version: ${{ matrix.python-version }} - - - name: Upgrade pip - if: steps.changes.outputs.stack == 'true' - run: | - pip install --upgrade pip uv==0.1.35 - uv --version - - - name: Get pip cache dir - if: steps.changes.outputs.stack == 'true' - id: pip-cache - shell: bash - run: | - echo "dir=$(uv cache dir)" >> $GITHUB_OUTPUT - - - name: pip cache - uses: actions/cache@v4 - if: steps.changes.outputs.stack == 'true' - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-uv-py${{ matrix.python-version }} - restore-keys: | - ${{ runner.os }}-uv-py${{ matrix.python-version }} - - - name: Install tox - if: steps.changes.outputs.stack == 'true' - run: | - pip install --upgrade tox tox-uv==1.5.1 - - - name: Show choco installed packages - if: steps.changes.outputs.stack == 'true' && matrix.os == 'windows' - uses: crazy-max/ghaction-chocolatey@v3 - with: - args: list --localonly - - - name: Install git - if: steps.changes.outputs.stack == 'true' && matrix.os == 'windows' - uses: crazy-max/ghaction-chocolatey@v3 - with: - args: install git.install --params "/GitAndUnixToolsOnPath /WindowsTerminal /NoAutoCrlf" -y - - - name: Install cmake - if: steps.changes.outputs.stack == 'true' && matrix.os == 'windows' - uses: crazy-max/ghaction-chocolatey@v3 - with: - args: install cmake.portable --installargs 'ADD_CMAKE_TO_PATH=System' -y - - - name: Check cmake version - if: steps.changes.outputs.stack == 'true' && matrix.os == 'windows' - run: | - cmake --version - shell: cmd - - - name: Install visualcpp-build-tools - if: steps.changes.outputs.stack == 'true' && matrix.os == 'windows' - uses: crazy-max/ghaction-chocolatey@v3 - with: - args: install visualstudio2019-workload-vctools -y - - - name: Install Docker Compose - if: steps.changes.outputs.stack == 'true' && runner.os == 'Linux' - shell: bash - run: | - mkdir -p ~/.docker/cli-plugins - DOCKER_COMPOSE_VERSION=v2.21.0 - curl -sSL https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose - chmod +x ~/.docker/cli-plugins/docker-compose - - - name: Docker on MacOS - if: steps.changes.outputs.stack == 'true' && matrix.os == 'macos-latest' - uses: crazy-max/ghaction-setup-docker@v3.1.0 - - - name: Docker Compose on MacOS - if: steps.changes.outputs.stack == 'true' && matrix.os == 'macos-latest' - shell: bash - run: | - brew install docker-compose - mkdir -p ~/.docker/cli-plugins - ln -sfn /usr/local/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose || true - docker compose version - - - name: Remove existing containers - if: steps.changes.outputs.stack == 'true' - continue-on-error: true - shell: bash - run: | - docker rm $(docker ps -aq) --force || true - docker volume prune -f || true - docker buildx use default || true - - # - name: Run integration tests - # if: steps.changes.outputs.stack == 'true' - # timeout-minutes: 60 - # env: - # HAGRID_ART: false - # PYTEST_MODULES: "${{ matrix.pytest-modules }}" - # HAGRID_FLAGS: "--tag=beta --test --build-src=dev" - # run: | - # tox -e stack.test.integration - - - name: Run integration tests - uses: nick-fields/retry@v3 - if: steps.changes.outputs.stack == 'true' - env: - HAGRID_ART: false - PYTEST_MODULES: "${{ matrix.pytest-modules }}" - HAGRID_FLAGS: "--tag=beta --test --build-src=dev" - with: - timeout_seconds: 1800 - max_attempts: 3 - command: tox -e stack.test.integration - continue-on-error: true - - - name: Reboot node - if: matrix.os == 'windows' && failure() - run: | - shutdown /r /t 1 - - - name: Run log collector - timeout-minutes: 5 - if: failure() - shell: bash - run: | - python ./scripts/container_log_collector.py - - - name: Get job name and url - id: job_name - if: failure() - shell: bash - run: | - echo "job_name=$(echo ${{ github.job }})" >> $GITHUB_OUTPUT - echo "url=$(echo ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_OUTPUT - - - name: Get current date - id: date - if: failure() - shell: bash - run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - - - name: Upload logs to GitHub - uses: actions/upload-artifact@master - if: failure() - with: - name: ${{ matrix.os }}-${{ steps.job_name.outputs.job_name }}-${{ matrix.pytest-modules }}-logs-${{ steps.date.outputs.date }} - path: ./logs/${{ steps.job_name.outputs.job_name}}/ - - - name: Get pull request url - id: pull_request - if: failure() - shell: bash - run: | - echo "url=$(echo ${{ github.event.pull_request.html_url }})" >> $GITHUB_OUTPUT - - - name: Job Report Status - if: github.repository == 'OpenMined/PySyft' && failure() - uses: ravsamhq/notify-slack-action@v2 - with: - status: ${{ job.status }} - notify_when: "failure" - notification_title: " {workflow} has {status_message}" - message_format: "${{matrix.os}} {emoji} *{job}* {status_message} in {run_url}" - footer: "Find the PR here ${{ steps.pull_request.outputs.url }}" - mention_users: "U01LNCACY03,U8KUAD396,UNMQ2SJSW,U01SAESBJA0" - mention_users_when: "failure,warnings" - env: - SLACK_WEBHOOK_URL: ${{ secrets.ACTION_MONITORING_SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/pr-tests-stack.yml b/.github/workflows/pr-tests-stack.yml index 32e227f6c3a..34620e3fa80 100644 --- a/.github/workflows/pr-tests-stack.yml +++ b/.github/workflows/pr-tests-stack.yml @@ -82,15 +82,78 @@ jobs: run: | tox -e backend.test.basecpu - pr-tests-stack-k8s: + pr-tests-syft-integration: strategy: max-parallel: 99 matrix: - # os: [ubuntu-latest, macos-latest, windows-latest, windows] - # os: [om-ci-16vcpu-ubuntu2204] os: [ubuntu-latest] python-version: ["3.12"] - pytest-modules: ["frontend network"] + pytest-modules: ["local_node"] + fail-fast: false + + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v4 + + - name: Check for file changes + uses: dorny/paths-filter@v3 + id: changes + with: + base: ${{ github.ref }} + token: ${{ github.token }} + filters: .github/file-filters.yml + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + if: steps.changes.outputs.stack == 'true' + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade pip + if: steps.changes.outputs.stack == 'true' + run: | + pip install --upgrade pip uv==0.1.35 + uv --version + + - name: Get pip cache dir + if: steps.changes.outputs.stack == 'true' + id: pip-cache + shell: bash + run: | + echo "dir=$(uv cache dir)" >> $GITHUB_OUTPUT + + - name: pip cache + uses: actions/cache@v4 + if: steps.changes.outputs.stack == 'true' + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-uv-py${{ matrix.python-version }} + restore-keys: | + ${{ runner.os }}-uv-py${{ matrix.python-version }} + + - name: Install tox + if: steps.changes.outputs.stack == 'true' + run: | + pip install --upgrade tox tox-uv==1.5.1 + + - name: Run Syft Integration Tests + if: steps.changes.outputs.stack == 'true' + timeout-minutes: 60 + env: + PYTEST_MODULES: "${{ matrix.pytest-modules }}" + GITHUB_CI: true + shell: bash + run: | + tox -e syft.test.integration + + pr-tests-integration-k8s: + strategy: + max-parallel: 99 + matrix: + os: [ubuntu-latest] + python-version: ["3.12"] + pytest-modules: ["frontend network container_workload"] fail-fast: false runs-on: ${{matrix.os}} @@ -171,15 +234,6 @@ jobs: chmod +x kubectl sudo install kubectl /usr/local/bin; - - name: Install k9s - if: steps.changes.outputs.stack == 'true' - run: | - # install k9s - wget https://github.com/derailed/k9s/releases/download/v0.32.4/k9s_Linux_amd64.tar.gz - tar -xvf k9s_Linux_amd64.tar.gz - chmod +x k9s - sudo install k9s /usr/local/bin; - - name: Install helm if: steps.changes.outputs.stack == 'true' run: | @@ -192,9 +246,9 @@ jobs: if: steps.changes.outputs.stack == 'true' timeout-minutes: 60 env: - HAGRID_ART: false PYTEST_MODULES: "${{ matrix.pytest-modules }}" GITHUB_CI: true + AZURE_BLOB_STORAGE_KEY: "${{ secrets.AZURE_BLOB_STORAGE_KEY }}" shell: bash run: | K3D_VERSION=v5.6.3 @@ -208,6 +262,7 @@ jobs: curl -sSL https://github.com/loft-sh/devspace/releases/download/${DEVSPACE_VERSION}/devspace-linux-amd64 -o ./devspace chmod +x devspace devspace version + tox -e stack.test.integration.k8s tox -e syft.build.helm tox -e syft.package.helm @@ -224,17 +279,164 @@ jobs: shell: bash run: | mkdir -p ./k8s-logs - kubectl describe all -A --context k3d-testgateway1 --namespace syft > ./k8s-logs/testgateway1-desc-${{ steps.date.outputs.date }}.txt - kubectl describe all -A --context k3d-testdomain1 --namespace syft > ./k8s-logs/testdomain1-desc-${{ steps.date.outputs.date }}.txt - kubectl logs -l app.kubernetes.io/name!=random --prefix=true --context k3d-testgateway1 --namespace syft > ./k8s-logs/testgateway1-logs-${{ steps.date.outputs.date }}.txt - kubectl logs -l app.kubernetes.io/name!=random --prefix=true --context k3d-testdomain1 --namespace syft > ./k8s-logs/testdomain1-logs-${{ steps.date.outputs.date }}.txt + kubectl describe all -A --context k3d-test-gateway-1 --namespace syft > ./k8s-logs/test-gateway-1-desc-${{ steps.date.outputs.date }}.txt + kubectl describe all -A --context k3d-test-domain-1 --namespace syft > ./k8s-logs/test-domain-1-desc-${{ steps.date.outputs.date }}.txt + kubectl logs -l app.kubernetes.io/name!=random --prefix=true --context k3d-test-gateway-1 --namespace syft > ./k8s-logs/test-gateway-1-logs-${{ steps.date.outputs.date }}.txt + kubectl logs -l app.kubernetes.io/name!=random --prefix=true --context k3d-test-domain-1 --namespace syft > ./k8s-logs/test-domain-1-logs-${{ steps.date.outputs.date }}.txt + ls -la ./k8s-logs + + - name: Upload logs to GitHub + uses: actions/upload-artifact@master + if: steps.changes.outputs.stack == 'true' && failure() + with: + name: k8s-logs-integration-${{ matrix.os }}-${{ steps.date.outputs.date }} + path: ./k8s-logs/ + + - name: Cleanup k3d + if: steps.changes.outputs.stack == 'true' && failure() + shell: bash + run: | + export PATH=`pwd`:$PATH + k3d cluster delete test-gateway-1 || true + k3d cluster delete test-domain-1 || true + k3d registry delete k3d-registry.localhost || true + + pr-tests-notebook-k8s: + strategy: + max-parallel: 99 + matrix: + os: [ubuntu-latest] + python-version: ["3.12"] + fail-fast: false + + runs-on: ${{matrix.os}} + + steps: + - name: Permission to home directory + run: | + sudo chown -R $USER:$USER $HOME + - uses: actions/checkout@v4 + - name: Check for file changes + uses: dorny/paths-filter@v3 + id: changes + with: + base: ${{ github.ref }} + token: ${{ github.token }} + filters: .github/file-filters.yml + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + if: steps.changes.outputs.stack == 'true' + with: + python-version: ${{ matrix.python-version }} + + - name: Add K3d Registry + run: | + sudo python ./scripts/patch_hosts.py --add-k3d-registry + + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: true + large-packages: false + + # free 10GB of space + - name: Remove unnecessary files + if: matrix.os == 'ubuntu-latest' + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + docker image prune --all --force + docker builder prune --all --force + docker system prune --all --force + + - name: Upgrade pip + if: steps.changes.outputs.stack == 'true' + run: | + pip install --upgrade pip uv==0.1.35 + uv --version + + - name: Get pip cache dir + if: steps.changes.outputs.stack == 'true' + id: pip-cache + shell: bash + run: | + echo "dir=$(uv cache dir)" >> $GITHUB_OUTPUT + + - name: pip cache + uses: actions/cache@v4 + if: steps.changes.outputs.stack == 'true' + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-uv-py${{ matrix.python-version }} + restore-keys: | + ${{ runner.os }}-uv-py${{ matrix.python-version }} + + - name: Install tox + if: steps.changes.outputs.stack == 'true' + run: | + pip install --upgrade tox tox-uv==1.5.1 + + - name: Install kubectl + if: steps.changes.outputs.stack == 'true' + run: | + # cleanup apt version + sudo apt remove kubectl || true + # install kubectl 1.27 + curl -LO https://dl.k8s.io/release/v1.27.2/bin/linux/amd64/kubectl + chmod +x kubectl + sudo install kubectl /usr/local/bin; + + - name: Install helm + if: steps.changes.outputs.stack == 'true' + run: | + # install helm + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + + - name: Run Notebooks Tests + if: steps.changes.outputs.stack == 'true' + timeout-minutes: 60 + env: + GITHUB_CI: true + shell: bash + run: | + K3D_VERSION=v5.6.3 + DEVSPACE_VERSION=v6.3.12 + # install k3d + wget https://github.com/k3d-io/k3d/releases/download/${K3D_VERSION}/k3d-linux-amd64 + mv k3d-linux-amd64 k3d + chmod +x k3d + export PATH=`pwd`:$PATH + k3d version + curl -sSL https://github.com/loft-sh/devspace/releases/download/${DEVSPACE_VERSION}/devspace-linux-amd64 -o ./devspace + chmod +x devspace + devspace version + tox -e stack.test.notebook.k8s + + - name: Get current timestamp + id: date + if: failure() + shell: bash + run: echo "date=$(date +%s)" >> $GITHUB_OUTPUT + + - name: Collect logs from k3d + if: steps.changes.outputs.stack == 'true' && failure() + shell: bash + run: | + mkdir -p ./k8s-logs + kubectl describe all -A --context k3d-test-gateway-1 --namespace syft > ./k8s-logs/test-gateway-1-desc-${{ steps.date.outputs.date }}.txt + kubectl describe all -A --context k3d-test-domain-1 --namespace syft > ./k8s-logs/test-domain-1-desc-${{ steps.date.outputs.date }}.txt + kubectl logs -l app.kubernetes.io/name!=random --prefix=true --context k3d-test-gateway-1 --namespace syft > ./k8s-logs/test-gateway-1-logs-${{ steps.date.outputs.date }}.txt + kubectl logs -l app.kubernetes.io/name!=random --prefix=true --context k3d-test-domain-1 --namespace syft > ./k8s-logs/test-domain-1-logs-${{ steps.date.outputs.date }}.txt ls -la ./k8s-logs - name: Upload logs to GitHub uses: actions/upload-artifact@master if: steps.changes.outputs.stack == 'true' && failure() with: - name: k8s-logs-${{ matrix.os }}-${{ steps.date.outputs.date }} + name: k8s-logs-notebook-${{ matrix.os }}-${{ steps.date.outputs.date }} path: ./k8s-logs/ - name: Cleanup k3d @@ -242,6 +444,6 @@ jobs: shell: bash run: | export PATH=`pwd`:$PATH - k3d cluster delete testgateway1 || true - k3d cluster delete testdomain1 || true + k3d cluster delete test-gateway-1 || true + k3d cluster delete test-domain-1 || true k3d registry delete k3d-registry.localhost || true diff --git a/.github/workflows/pr-tests-syft.yml b/.github/workflows/pr-tests-syft.yml index cc8fcb00ecb..046dea143e0 100644 --- a/.github/workflows/pr-tests-syft.yml +++ b/.github/workflows/pr-tests-syft.yml @@ -39,7 +39,7 @@ jobs: # run: | # sudo chown -R $USER:$USER $HOME - name: "clean .git/config" - if: matrix.os == 'windows' + if: matrix.os == 'windows-latest' continue-on-error: true shell: bash run: | @@ -86,7 +86,7 @@ jobs: # - name: Docker on MacOS # if: steps.changes.outputs.syft == 'true' && matrix.os == 'macos-latest' - # uses: crazy-max/ghaction-setup-docker@v3.1.0 + # uses: crazy-max/ghaction-setup-docker@v3.2.0 # with: # set-host: true @@ -134,7 +134,7 @@ jobs: # run: | # sudo chown -R $USER:$USER $HOME - name: "clean .git/config" - if: matrix.os == 'windows' + if: matrix.os == 'windows-latest' continue-on-error: true shell: bash run: | @@ -202,7 +202,7 @@ jobs: matrix: os: [ubuntu-latest] python-version: ["3.10", "3.11", "3.12"] - deployment-type: ["k8s"] + deployment-type: ["remote"] notebook-paths: ["api/0.8"] fail-fast: false @@ -278,7 +278,7 @@ jobs: - name: Docker on MacOS if: (steps.changes.outputs.stack == 'true' || steps.changes.outputs.notebooks == 'true') && matrix.os == 'macos-latest' - uses: crazy-max/ghaction-setup-docker@v3.1.0 + uses: crazy-max/ghaction-setup-docker@v3.2.0 - name: Docker Compose on MacOS if: (steps.changes.outputs.stack == 'true' || steps.changes.outputs.notebooks == 'true') && matrix.os == 'macos-latest' diff --git a/.github/workflows/test-github-arc.yml b/.github/workflows/test-github-arc.yml deleted file mode 100644 index 4f3dfacfa29..00000000000 --- a/.github/workflows/test-github-arc.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Actions Runner Controller Demo -on: - workflow_dispatch: - -jobs: - Test-Github-ARC-x64: - # You need to use the INSTALLATION_NAME from the previous step - runs-on: sh-arc-linux-x64 - steps: - - name: "Test Github ARC" - run: | - echo "πŸŽ‰ This job uses runner scale set runners!" - - - name: "Check Architecture" - run: | - uname -a - - Test-Github-ARC-arm64: - # You need to use the INSTALLATION_NAME from the previous step - runs-on: sh-arc-linux-arm64 - steps: - - name: "Test Github ARC" - run: | - echo "πŸŽ‰ This job uses runner scale set runners!" - - - name: "Check Architecture" - run: | - uname -a diff --git a/.gitignore b/.gitignore index ae0b09e4342..fc3d10b8733 100644 --- a/.gitignore +++ b/.gitignore @@ -28,9 +28,6 @@ build # docker compose volumes docker/data/* -# hagrid temps -packages/hagrid/syft -packages/hagrid/grid # vagrant .vagrant @@ -60,7 +57,6 @@ notebooks/**/*.pkl k3d-registry .envfile -packages/hagrid/.envfile # rendered template dir diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 584f776b221..00000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,11 +0,0 @@ -tasks: - - init: pip install -e packages/hagrid - command: hagrid quickstart -ports: - - name: Jupyter - port: 8888 - visibility: public - - name: Nodes - port: 8081-8083 - onOpen: open-browser - visibility: public diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d243ca60d91..2b4c5fa22ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,7 +52,6 @@ repos: packages/syft/src/syft/proto.*| packages/syft/tests/syft/lib/python.*| packages/grid.*| - packages/hagrid.*| packages/syft/src/syft/federated/model_serialization/protos.py )$ @@ -95,31 +94,6 @@ repos: - id: ruff-format types_or: [python, pyi, jupyter] - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 - hooks: - - id: mypy - name: "mypy: hagrid" - always_run: true - files: ^packages/hagrid - args: [ - "--ignore-missing-imports", - "--scripts-are-modules", - "--disallow-incomplete-defs", - "--no-implicit-optional", - "--warn-unused-ignores", - "--warn-redundant-casts", - "--strict-equality", - "--warn-unreachable", - # "--disallow-untyped-decorators", - "--disallow-untyped-defs", - "--disallow-untyped-calls", - "--namespace-packages", - "--install-types", - "--non-interactive", - "--config-file=tox.ini", - ] - - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.8.0 hooks: @@ -206,7 +180,7 @@ repos: rev: "v3.0.0-alpha.9-for-vscode" hooks: - id: prettier - exclude: ^(packages/grid/helm|packages/grid/frontend/pnpm-lock.yaml|packages/hagrid/hagrid/manifest_template.yml|packages/syft/tests/mongomock) + exclude: ^(packages/grid/helm|packages/grid/frontend/pnpm-lock.yaml|packages/syft/tests/mongomock) # - repo: meta # hooks: diff --git a/README.md b/README.md index 25230bb43f9..48e0a38c5cb 100644 --- a/README.md +++ b/README.md @@ -108,18 +108,11 @@ For Google GKE we need the [`gce` annotation](https://cloud.google.com/kubernete helm install ... --set ingress.class="gce" ``` -## Deploy to a Container Engine or Cloud +## Note: -1. Install our handy πŸ›΅ cli tool which makes deploying a Domain or Gateway server to Docker or VM a one-liner: - `pip install -U hagrid` +🚨 Our deployment tool `Hagrid` has been `Deprecated`. For the updated deployment options kindly refer to -2. Then run our interactive jupyter Install πŸ§™πŸ½β€β™‚οΈ WizardBETA: - `hagrid quickstart` - -3. In the tutorial you will learn how to install and deploy: - `PySyft` = our `numpy`-like 🐍 Python library for computing on `private data` in someone else's `Domain` - - `PyGrid` = our 🐳 `docker` / 🐧 `vm` `Domain` & `Gateway` Servers where `private data` lives +- πŸ“š Deployments ## Docs and Support @@ -128,10 +121,8 @@ helm install ... --set ingress.class="gce" # Install Notes -- HAGrid 0.3 Requires: 🐍 `python` πŸ™ `git` - Run: `pip install -U hagrid` -- Interactive Install πŸ§™πŸ½β€β™‚οΈ WizardBETA Requires πŸ›΅ `hagrid`: - Run: `hagrid quickstart` - PySyft 0.8.1 Requires: 🐍 `python 3.10 - 3.12` - Run: `pip install -U syft` -- PyGrid Requires: 🐳 `docker`, 🦦 `podman` or ☸️ `kubernetes` - Run: `hagrid launch ...` +- PyGrid Requires: 🐳 `docker`, 🦦 `podman` or ☸️ `kubernetes` # Versions @@ -154,13 +145,9 @@ Deprecated: PySyft and PyGrid use the same `version` and its best to match them up where possible. We release weekly betas which can be used in each context: -PySyft (Stable): `pip install -U syft` -PyGrid (Stable) `hagrid launch ... tag=latest` - -PySyft (Beta): `pip install -U syft --pre` -PyGrid (Beta): `hagrid launch ... tag=beta` +PySyft (Stable): `pip install -U syft` -HAGrid is a cli / deployment tool so the latest version of `hagrid` is usually the best. +PySyft (Beta): `pip install -U syft --pre` # What is Syft? diff --git a/notebooks/admin/Custom API + Custom Worker.ipynb b/notebooks/admin/Custom API + Custom Worker.ipynb index bbf47476301..ef6bde6ab9c 100644 --- a/notebooks/admin/Custom API + Custom Worker.ipynb +++ b/notebooks/admin/Custom API + Custom Worker.ipynb @@ -36,8 +36,8 @@ "metadata": {}, "outputs": [], "source": [ - "## k8s mode\n", - "# os.environ[\"ORCHESTRA_DEPLOYMENT_TYPE\"] = \"k8s\"\n", + "## remote mode\n", + "# os.environ[\"ORCHESTRA_DEPLOYMENT_TYPE\"] = \"remote\"\n", "# os.environ[\"DEV_MODE\"] = \"True\"\n", "domain_client = sy.login(\n", " email=\"info@openmined.org\",\n", diff --git a/notebooks/api/0.8/01-submit-code.ipynb b/notebooks/api/0.8/01-submit-code.ipynb index ec11b60af9f..761d1a96e7a 100644 --- a/notebooks/api/0.8/01-submit-code.ipynb +++ b/notebooks/api/0.8/01-submit-code.ipynb @@ -482,7 +482,7 @@ "outputs": [], "source": [ "# Once we start the project, it will submit the project along with the code request to the Domain Server\n", - "project = new_project.start()\n", + "project = new_project.send()\n", "project" ] }, @@ -599,7 +599,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.10.13" }, "toc": { "base_numbering": 1, diff --git a/notebooks/api/0.8/06-multiple-code-requests.ipynb b/notebooks/api/0.8/06-multiple-code-requests.ipynb index 6e19bc6731c..4be948cc00b 100644 --- a/notebooks/api/0.8/06-multiple-code-requests.ipynb +++ b/notebooks/api/0.8/06-multiple-code-requests.ipynb @@ -250,7 +250,7 @@ }, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "\n", "project" ] @@ -578,7 +578,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" + "version": "3.10.13" }, "toc": { "base_numbering": 1, diff --git a/notebooks/api/0.8/09-blob-storage.ipynb b/notebooks/api/0.8/09-blob-storage.ipynb index 713fd53f6f4..93491499896 100644 --- a/notebooks/api/0.8/09-blob-storage.ipynb +++ b/notebooks/api/0.8/09-blob-storage.ipynb @@ -36,7 +36,6 @@ "node = sy.orchestra.launch(\n", " name=\"test-domain-1\",\n", " dev_mode=True,\n", - " in_memory_workers=True,\n", " reset=True,\n", " create_producer=True,\n", ")" diff --git a/notebooks/api/0.8/10-container-images.ipynb b/notebooks/api/0.8/10-container-images.ipynb index 84fd1a340ab..cb4bd49cf86 100644 --- a/notebooks/api/0.8/10-container-images.ipynb +++ b/notebooks/api/0.8/10-container-images.ipynb @@ -91,8 +91,7 @@ "# Disable inmemory worker for container stack\n", "running_as_container = os.environ.get(\"ORCHESTRA_DEPLOYMENT_TYPE\") in (\n", " \"container_stack\",\n", - ")\n", - "in_memory_workers = not running_as_container" + ")" ] }, { @@ -106,7 +105,6 @@ " name=\"test-domain-1\",\n", " dev_mode=True,\n", " create_producer=True,\n", - " in_memory_workers=in_memory_workers,\n", " reset=True,\n", " port=8081,\n", ")" @@ -1485,7 +1483,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.7" } }, "nbformat": 4, diff --git a/notebooks/api/0.8/11-container-images-k8s.ipynb b/notebooks/api/0.8/11-container-images-k8s.ipynb index c9663acd3ad..77cd71c7912 100644 --- a/notebooks/api/0.8/11-container-images-k8s.ipynb +++ b/notebooks/api/0.8/11-container-images-k8s.ipynb @@ -45,7 +45,7 @@ "metadata": {}, "outputs": [], "source": [ - "os.environ[\"ORCHESTRA_DEPLOYMENT_TYPE\"] = \"k8s\"\n", + "os.environ[\"ORCHESTRA_DEPLOYMENT_TYPE\"] = \"remote\"\n", "os.environ[\"DEV_MODE\"] = \"True\"\n", "\n", "# Uncomment this to add custom values\n", diff --git a/notebooks/api/0.8/12-custom-api-endpoint.ipynb b/notebooks/api/0.8/12-custom-api-endpoint.ipynb index dd867dc3757..f84ca9c5c3f 100644 --- a/notebooks/api/0.8/12-custom-api-endpoint.ipynb +++ b/notebooks/api/0.8/12-custom-api-endpoint.ipynb @@ -33,7 +33,6 @@ " dev_mode=True,\n", " create_producer=True,\n", " n_consumers=3,\n", - " in_memory_workers=True,\n", " reset=True,\n", " port=8081,\n", ")\n", diff --git a/notebooks/quickstart/00-quickstart.ipynb b/notebooks/quickstart/00-quickstart.ipynb deleted file mode 100644 index d5b14e2d8e0..00000000000 --- a/notebooks/quickstart/00-quickstart.ipynb +++ /dev/null @@ -1,115 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "9c0d3bd0-cd25-4794-81de-c413f260de3c", - "metadata": {}, - "source": [ - "# HAGrid Quickstart BETA" - ] - }, - { - "cell_type": "markdown", - "id": "b4813d9f-daec-4954-96aa-90c01159d396", - "metadata": {}, - "source": [ - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " πŸ“š quickstart\n", - " \n", - " πŸ§™β€β™‚οΈ Install Wizard\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "a50d74d3-66f0-4181-8c2f-b07fdf6b0979", - "metadata": {}, - "source": [ - " " - ] - }, - { - "cell_type": "markdown", - "id": "df038714-df01-4c56-a84c-b66a42d6cd81", - "metadata": {}, - "source": [ - "
Step 1. Run quickstart
" - ] - }, - { - "cell_type": "markdown", - "id": "3e0dd95e-38f8-46d4-b75a-d1bc9c6ac103", - "metadata": {}, - "source": [ - "Simply `import` and run `quickstart` by clicking in the grey cell below πŸ‘‡πŸ½ and pressing `Shift` + `Return` on your keyboard, or use the `Run` menu at the top of the window." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c71be622-bb83-4d32-aeaa-00f2aca6ee80", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# third party\n", - "from hagrid import quickstart\n", - "\n", - "quickstart" - ] - }, - { - "cell_type": "markdown", - "id": "ec809a11-95c7-4783-a397-58c93eb19dcf", - "metadata": {}, - "source": [ - "
Step 2. Download a Tutorial
" - ] - }, - { - "cell_type": "markdown", - "id": "27a1c075-3082-408e-8486-ab9df41a8442", - "metadata": {}, - "source": [ - "Above you will see a list of available tutorials, simply add the name in `quotes` into the `quickstart.download` function and run the cell below just like before." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f5507b20-5149-4b6d-b7e0-0883a2358ceb", - "metadata": {}, - "outputs": [], - "source": [ - "# paste and run any commands here" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/quickstart/01-install-wizard.ipynb b/notebooks/quickstart/01-install-wizard.ipynb deleted file mode 100644 index 7050c8b5b8f..00000000000 --- a/notebooks/quickstart/01-install-wizard.ipynb +++ /dev/null @@ -1,500 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "5198b587-cf2f-4354-bdbb-08f9ecb46abf", - "metadata": {}, - "source": [ - "# HAGrid Install πŸ§™πŸ½β€β™‚οΈ Wizard BETA" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "a156040f-39cc-4660-a32a-5d2c2ff586b7", - "metadata": {}, - "source": [ - "\n", - " \n", - " \n", - " \n", - "
\n", - " πŸ“š quickstart / 01-install-wizard.ipynb\n", - "
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "cf271e52-b3aa-48f6-b85f-353b328dd463", - "metadata": {}, - "source": [ - "Welcome to the HAGrid Quickstart Install Wizard. \n", - "There are several different components required to use `Syft`. To make setup easy this wizard will automatically detect your current system configuration and make recommendations on what you need to complete setup.\n", - "\n", - "Run each step by clicking in the grey cell below πŸ‘‡πŸ½ and pressing `Shift` + `Return` on your keyboard, or use the `Run` menu at the top of the window.\n", - "\n", - "**How the Install Wizard Works** \n", - "At each step the πŸ§™πŸ½β€β™‚οΈ Wizard will try to find various software and packages on your machine. \n", - "If you see an item marked with a ❌ red cross and the message `🚨 Some issues were found` it should include a description of the issue, a solution and optionally a way to resolve the solution directly by running a command. These commands can br Copy + Pasted into a cell and ran here, or if you know how to use the terminal on your computer simply remove the `!` at the start of the command and paste it there instead. After you have resolved the issue you can run the step again to verify it is fixed with a βœ… green tick." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d657c28d-5388-45cc-ba37-f5e6401c0c88", - "metadata": {}, - "source": [ - "
Step 1. Import Install πŸ§™πŸ½β€β™‚οΈ Wizard BETA
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ad558ade-ee9b-4c61-8cbd-68eadd2f07fa", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# third party\n", - "import hagrid\n", - "from hagrid import wizard" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "5568b6bd-0015-458a-bb33-be9a927d0ffa", - "metadata": {}, - "source": [ - "
Step 2. HAGrid Updates
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f5ab8626-0da1-4a8e-be94-8871724da34c", - "metadata": {}, - "source": [ - "It's a good idea to keep `HAGrid` updated as we push out fixes and features very frequently. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a546b80e-ec38-4662-9f6b-0caa5b581bdc", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "wizard.check_hagrid" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8060ab70-f5e1-4a09-bcb5-0646c84c478c", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# paste and run any commands here" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "871d2ad4-77ff-4381-a26c-036717d948b8", - "metadata": {}, - "source": [ - "
Step 3. Installing PySyft
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1998c1a5-2fb4-4730-98e1-a4e2c360c5b7", - "metadata": {}, - "source": [ - "`PySyft` is a python library which requires Python 3.9+." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b954a90e-ad48-417f-bbe7-cd2d75cecc5e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "wizard.check_syft" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "554dfac3-9022-44f2-8c32-30af1b3a8dd8", - "metadata": {}, - "outputs": [], - "source": [ - "# paste and run any commands here" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "3d82f627-6fb6-4f03-a0c4-2ef68c6bf7f9", - "metadata": { - "tags": [] - }, - "source": [ - "
Step 4. Python Server
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7278fdb2-f1ba-4f12-a10b-50a43270c59f", - "metadata": { - "tags": [] - }, - "source": [ - "To do the `quickstart` tutorials, you can just run a basic `PyGrid` domain directly in python.\n", - "\n", - "You can do this either from `jupyter` / `python` or from the command line." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b205fc3-c84e-4638-82cf-0bfa25b206b5", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# syft absolute\n", - "import syft as sy\n", - "\n", - "sy.requires(\">=0.8.2.b0\")\n", - "node = sy.orchestra.launch(name=\"test-domain-1\", port=8080, dev_mode=True, reset=True)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "aa136f0a-debc-4219-858b-1814ed3cad46", - "metadata": { - "tags": [] - }, - "source": [ - "We can now log into the node with the default credentials." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4dd63cc9-5fe1-42af-9863-65a74eb2fc28", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "domain = sy.login(email=\"info@openmined.org\", password=\"changethis\", port=8080)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "5259b7f2-b480-4346-9cc7-86305fef76b5", - "metadata": {}, - "source": [ - "Let's see whats available on the API." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "312d1e50-99c4-4120-9dfc-caa56cee62fd", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "domain.api" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cda9bf88-4273-48aa-b656-005a8f456e6c", - "metadata": {}, - "outputs": [], - "source": [ - "# paste and run any commands here" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "956202c6-1d24-4889-aeda-6a7efe4a4055", - "metadata": {}, - "source": [ - "Okay, now let's shutdown the server." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cb2a0096-6c15-4ac9-9446-6532c1524381", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "node.land()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7b2f1e6c-e469-4be5-b771-3724f92d2305", - "metadata": {}, - "source": [ - "πŸ‘ˆπŸΏ Click here to go back to Quickstart Home" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "efc6ebc7-43e6-4612-bf44-7ce14e488d01", - "metadata": {}, - "source": [ - "
\n", - "
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e05d1893-3347-41a3-8ee3-c04fd8f12d0f", - "metadata": {}, - "source": [ - "
Step 5. Docker Setup (Optional)
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2fa85963-4635-4ce3-8be6-36b308aa26d5", - "metadata": {}, - "source": [ - "`PyGrid` can also run as a set of containerized services on a container host. Let's ask `hagrid` to check if we have all the right dependencies installed. If we don't it will make some recommendations on what to install." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "83f58a56-7c5e-4dfb-94c6-0fd1ce950a3f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# third party\n", - "from hagrid import wizard\n", - "\n", - "wizard.check_docker" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "98b47e48-582f-4261-838b-1b7b2844f945", - "metadata": {}, - "outputs": [], - "source": [ - "# paste and run any commands here" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "ee41f9c5-8014-41e6-b9a1-542694cf2d31", - "metadata": {}, - "source": [ - "
Step 6. Start a Test Domain
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "068440ad-f0a0-4a71-bcca-52e598bf1968", - "metadata": {}, - "source": [ - "You are now ready to start a `domain` on your local machine with 🐳 Docker. Simply run the next cell and wait until it is completed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f06b0bf-c8c1-4b3c-87c8-b0f0809fcbfc", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# syft absolute\n", - "import syft as sy\n", - "\n", - "sy.requires(\">=0.8.2.b0\")\n", - "node = sy.orchestra.launch(\n", - " name=\"test-domain-1\", node_type=\"domain\", port=8081, tag=\"beta\", verbose=True\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7d5c9892-1b70-4a68-b41d-8c9c06c19df0", - "metadata": {}, - "source": [ - "
Step 7. Check Domain Health
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "b7846431-69b1-46bb-a611-e2ce47eb277f", - "metadata": {}, - "source": [ - "To ensure our domain has finished starting we can ask `hagrid` to check its health for us. Run the below cell to check your `domain` on localhost. You can also visit the links to see the `UI` and `api` endpoints in your browser." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "71c1fd7d-fb7b-43dc-9608-053a6313b1b4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "hagrid.check(\"localhost:8081\", timeout=120)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "91148fc9-a26f-4964-bb17-efd5a26f465d", - "metadata": {}, - "source": [ - "
Step 8. Domain Login
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "07093a4b-d463-4fe5-9861-09e7d32b63e0", - "metadata": {}, - "source": [ - "We now log into the Domain Node using the default admin username and password." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "01126584-3449-4c4b-9fe4-1c727a7a0ee3", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# syft absolute\n", - "import syft as sy\n", - "\n", - "sy.requires(\">=0.8.2.b0\")\n", - "domain = sy.login(email=\"info@openmined.org\", password=\"changethis\", port=8081)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d4d7ccbb-0b48-41cf-bf7c-9b41f77924d0", - "metadata": {}, - "source": [ - "
Step 9. Shutdown Domain
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "8e3fc2ee-5247-4fa1-9e31-9d9464765e0d", - "metadata": {}, - "source": [ - "If your domain started correctly you are now done with the Install Wizard and ready to do some tutorials. We can shutdown this domain by running the `hagrid` land command in the below cell. If you are done now you can go ahead and shutdown your domain, or if you would prefer to keep it running skip this step." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f0d12f71-26eb-4317-a8ea-7e45579b7b59", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "node.land()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "c288eae3-6109-4b26-ac68-7ab772518919", - "metadata": {}, - "source": [ - "
βœ… Install πŸ§™πŸ½β€β™‚οΈ Wizard Complete
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "6663f1cc-648c-43e0-aca8-4d55039e8a6d", - "metadata": {}, - "source": [ - "πŸ‘ˆπŸΏ Click here to go back to Quickstart Home" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.1" - }, - "vscode": { - "interpreter": { - "hash": "1e7e90b573593ba97b24c163dae9a6c9173808a1bc968e87367841cbed28165e" - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/quickstart/img/edit.png b/notebooks/quickstart/img/edit.png deleted file mode 100644 index 3ceaaf9438d..00000000000 Binary files a/notebooks/quickstart/img/edit.png and /dev/null differ diff --git a/notebooks/quickstart/img/head.png b/notebooks/quickstart/img/head.png deleted file mode 100644 index 9d220749f31..00000000000 Binary files a/notebooks/quickstart/img/head.png and /dev/null differ diff --git a/notebooks/quickstart/img/run.png b/notebooks/quickstart/img/run.png deleted file mode 100644 index c3a678a9fd1..00000000000 Binary files a/notebooks/quickstart/img/run.png and /dev/null differ diff --git a/notebooks/tutorials/data-engineer/01-setting-up-dev-mode.ipynb b/notebooks/tutorials/data-engineer/01-setting-up-dev-mode.ipynb deleted file mode 100644 index ed84817235f..00000000000 --- a/notebooks/tutorials/data-engineer/01-setting-up-dev-mode.ipynb +++ /dev/null @@ -1,370 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Setting up Dev Mode" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "If you would like to work on the PySyft codebase, you can set up PySyft in dev mode. You will need to clone the repository, install syft locally and run the code you installed" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "## Cloning the Repo" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "First, we start by cloning the repo" - ] - }, - { - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "If you have an SSH key enabled in your github account, use" - ] - }, - { - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "`git clone git@github.com:OpenMined/PySyft.git`" - ] - }, - { - "cell_type": "markdown", - "id": "6", - "metadata": {}, - "source": [ - "Otherwise use" - ] - }, - { - "cell_type": "markdown", - "id": "7", - "metadata": {}, - "source": [ - "`git clone https://github.com/OpenMined/PySyft.git`" - ] - }, - { - "cell_type": "markdown", - "id": "8", - "metadata": {}, - "source": [ - "## Installing Syft" - ] - }, - { - "cell_type": "markdown", - "id": "9", - "metadata": {}, - "source": [ - "To install Syft `cd` into the directory in which you cloned PySyft and type\n", - "\n", - "```bash\n", - "pip install -e packages/syft\n", - "```\n", - "\n", - "This installs `syft` in editable mode, such any change in code are reflected in your environment." - ] - }, - { - "cell_type": "markdown", - "id": "10", - "metadata": {}, - "source": [ - "## Running Tox Tests" - ] - }, - { - "cell_type": "markdown", - "id": "11", - "metadata": {}, - "source": [ - "[Tox](https://tox.wiki/en/latest/) is a project that \"aims to automate and standardize testing in Python\". For PySyft development, it is used to simplify testing and setting up several environment in a way that works for every developer working on PySyft. You can list the commands that you can execute using `tox-l`, which will give a result similar to this" - ] - }, - { - "cell_type": "markdown", - "id": "12", - "metadata": {}, - "source": [ - "```\n", - "> tox -l\n", - "\n", - "hagrid.publish\n", - "lint\n", - "stack.test.integration\n", - "syft.docs\n", - "syft.jupyter\n", - "syft.publish\n", - "syft.test.security\n", - "syft.test.unit\n", - "syft.test.notebook\n", - "stack.test.notebook\n", - "stack.test.vm\n", - "frontend.test.unit\n", - "frontend.test.e2e\n", - "frontend.generate.types\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "13", - "metadata": {}, - "source": [ - "This shows us the list of environments that are specified for PySyft. To see what these environments do, have a look at the `tox.ini` file in the main PySyft repo." - ] - }, - { - "cell_type": "markdown", - "id": "14", - "metadata": {}, - "source": [ - "You can run an environment using `tox -e `. For instance, to run the unit tests, run" - ] - }, - { - "cell_type": "markdown", - "id": "15", - "metadata": {}, - "source": [ - "```\n", - "tox -e syft.test.unit\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "16", - "metadata": {}, - "source": [ - "This tox environment is relatively simple, and just uses pytest to run all the tests for the syft packages. However, some environments are more complicated, and run a series of commands that start multiple processes, docker containers and set up a lot of infrastructure before running the tests. The good thing is that with tox, you dont need to worry about that, you can just run the commands." - ] - }, - { - "cell_type": "markdown", - "id": "17", - "metadata": {}, - "source": [ - "## Using Jupyter Environment" - ] - }, - { - "cell_type": "markdown", - "id": "18", - "metadata": {}, - "source": [ - "Pysyft has a tox command to set up a local jupyter notebook environment, which is useful for development." - ] - }, - { - "cell_type": "markdown", - "id": "19", - "metadata": {}, - "source": [ - "```\n", - "tox -e syft.jupyter\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "20", - "metadata": {}, - "source": [ - "PySyft makes extensive use of jupyter notebook, and a lot of developers use it for experiments when writing code. It can be useful to setup a local gitignore (only for you, not pushed to git) to have a playground where you can experiment, without needing to push files to git, or change the .gitignore. You can do this by adding a folder to your `.git/info/exclude` file, which works similar to the `.gitignore` file, e.g. if we add\n", - "```\n", - "notebooks/experimental/* \n", - "```\n", - "to `.git/info/exclude`, git wont sync the changes to the `experimental` folder to github\n", - "\n", - "`Note:` For developers in MS Windows, before development make sure that your development path does not contain any white spaces in between.\n", - "\n", - "Example:\n", - " \n", - "**Invalid Path:** `D:/test space/new env/openmined/PySyft`\n", - "\n", - "**Valid Path:** `D:/test-space/new_env/openmined/PySyft`\n", - "\n", - "The issue with paths containing spaces causing problems on Windows is due to the way that Windows handles file paths, but as long as the development path is free of white spaces, you are good to go. This is not a specific issue related to PySyft." - ] - }, - { - "cell_type": "markdown", - "id": "21", - "metadata": {}, - "source": [ - "## Working with Python Domain" - ] - }, - { - "cell_type": "markdown", - "id": "22", - "metadata": {}, - "source": [ - "PySyft enables a network of computers to connect to each other and do privacy preserving data analysis. The Nodes in the network that hold some data are called `Domains`. When we develop with PySyft, it is very common to start a domain as the first step. `PySyft` makes it very easy to develop against a domain in a notebook by providing an interface (`sy.orchestra`) that allows you to start a domain with a webserver in a notebook in the background, which is a lightweight version of a Domain that would be used in production. You can specify options such as what kind of database you are using, whether you want to use networking and how many processes you want to use. You can launch a Domain by simply executing:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "23", - "metadata": {}, - "outputs": [], - "source": [ - "# syft absolute\n", - "import syft as sy" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "node = sy.orchestra.launch(\n", - " name=\"dev-mode-example-domain-1\", port=8020, reset=True, dev_mode=True\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "25", - "metadata": {}, - "source": [ - "If we dont need a webserver (for development this is true in many cases), we can omit the port and use. \n", - "```\n", - "node = sy.orchestra.launch(name=\"dev-mode-example-domain-1\", dev_mode=True, reset=True)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "26", - "metadata": {}, - "source": [ - "**One of the benefits of not using a port is that you can use a debugger and set breakpoints within api calls. This makes debugging way faster in many cases**" - ] - }, - { - "cell_type": "markdown", - "id": "27", - "metadata": {}, - "source": [ - "Now, we are ready to start using the domain. The domain comes with standard login credentials for the admin (just for development)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28", - "metadata": {}, - "outputs": [], - "source": [ - "client = node.login(email=\"info@openmined.org\", password=\"changethis\")" - ] - }, - { - "cell_type": "markdown", - "id": "29", - "metadata": {}, - "source": [ - "Once you are logged in, you are ready to start using the domain, for instance for creating a dataset (this one is empty, just as a example)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "30", - "metadata": {}, - "outputs": [], - "source": [ - "dataset = sy.Dataset(name=\"my dataset\", asset_list=[])\n", - "client.upload_dataset(dataset)" - ] - }, - { - "cell_type": "markdown", - "id": "31", - "metadata": {}, - "source": [ - "Lastly to stop or terminate your Domain, we can execute the following command:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "32", - "metadata": {}, - "outputs": [], - "source": [ - "node.land()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "33", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.16" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": true - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/02-deployment-types.ipynb b/notebooks/tutorials/data-engineer/02-deployment-types.ipynb deleted file mode 100644 index b4c43e5929d..00000000000 --- a/notebooks/tutorials/data-engineer/02-deployment-types.ipynb +++ /dev/null @@ -1,378 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Deployment Types" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1", - "metadata": {}, - "outputs": [], - "source": [ - "# syft absolute\n", - "import syft as sy" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "## Dev Python Domain\n" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "Syft supports creating a Python domain in editable mode.\n", - "This is used mainly for experimental and development purposes.\n", - "In __Dev Python Domain__ the domain instance runs locally using the SQLite as the main storage.\n", - "This enables faster development and requires less recources to operate.\n", - "\n", - "The __Dev Python Domain__ supports two options:\n", - "1. Memory node - full `syft` functionality __locally__, SQLite as a local storage.\n", - "2. Webserver node - full `syft` functionality with API \n", - "\n", - "__When you need this?__
\n", - "_When you want to develop Syft or try-out new funcitonality from separate branch._\n", - "\n", - "__Prerequistes:__
\n", - "1. Syft repository pulled from Github - [github.com/OpenMined/PySyft](https://github.com/OpenMined/PySyft)\n", - "\n", - "For broader explanation refer to the notebook [01-setting-dev-mode.ipynb](https://github.com/OpenMined/PySyft/blob/dev/notebooks/tutorials/data-engineer/01-setting-up-dev-mode.ipynb)\n", - "\n", - "To launch the local __Dev Python Domain__ use the following steps:" - ] - }, - { - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "#### 1.1 Launch Dev Memory Node" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5", - "metadata": {}, - "outputs": [], - "source": [ - "memory_node = sy.Orchestra.launch(\n", - " name=\"Arbitrary Dev Node\",\n", - " dev_mode=True,\n", - " reset=True,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6", - "metadata": {}, - "outputs": [], - "source": [ - "assert memory_node is not None" - ] - }, - { - "cell_type": "markdown", - "id": "7", - "metadata": {}, - "source": [ - "#### 1.2 Launch Dev Webserver Node" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8", - "metadata": {}, - "outputs": [], - "source": [ - "webserver_node = sy.Orchestra.launch(\n", - " name=\"Arbitrary Webserver Dev Node\", dev_mode=True, reset=True, port=8081\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9", - "metadata": {}, - "outputs": [], - "source": [ - "assert webserver_node is not None" - ] - }, - { - "cell_type": "markdown", - "id": "10", - "metadata": {}, - "source": [ - "#### 2. Login Into Nodes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "11", - "metadata": {}, - "outputs": [], - "source": [ - "memory_node_client = memory_node.login(\n", - " email=\"info@openmined.org\", password=\"changethis\"\n", - ")\n", - "memory_node_client" - ] - }, - { - "cell_type": "markdown", - "id": "12", - "metadata": {}, - "source": [ - "#### 3. Landing Memory and Webserver Node" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "13", - "metadata": {}, - "outputs": [], - "source": [ - "memory_node.land()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "14", - "metadata": {}, - "outputs": [], - "source": [ - "webserver_node.land()" - ] - }, - { - "cell_type": "markdown", - "id": "15", - "metadata": {}, - "source": [ - "----" - ] - }, - { - "cell_type": "markdown", - "id": "16", - "metadata": {}, - "source": [ - "## Single Container / Enclave (TBD)" - ] - }, - { - "cell_type": "markdown", - "id": "17", - "metadata": {}, - "source": [ - "Single Container deployment is used when fast and painless deployment of `syft` with all essential functionality is needed. This deployment type contains the `syft` and SQLite as a light-weight database in a single container.\n", - "\n", - "__When you need this?__
\n", - "_When you quickly want to test syft in a single container._\n", - "\n", - "__Prerequistes:__
\n", - "1. Syft repository pulled from Github - [github.com/OpenMined/PySyft](https://github.com/OpenMined/PySyft)\n", - "1. Docker Installed - [docs.docker.com/get-docker](https://docs.docker.com/get-docker/)\n" - ] - }, - { - "cell_type": "markdown", - "id": "18", - "metadata": {}, - "source": [ - "#### Deploy Syft in Single Container Mode" - ] - }, - { - "cell_type": "markdown", - "id": "19", - "metadata": {}, - "source": [ - "Enter the PySyft Repository and run the following command\n", - "\n", - "`docker run -it -e DEFAULT_ROOT_PASSWORD=secret -e PORT=8080 -p 8080:8080 openmined/grid-enclave:0.8.1`\n", - "\n", - "----" - ] - }, - { - "cell_type": "markdown", - "id": "20", - "metadata": {}, - "source": [ - "## Full Container Stack" - ] - }, - { - "cell_type": "markdown", - "id": "21", - "metadata": {}, - "source": [ - "Syft can operate as a container stack. This setting consider deployment of following containers:\n", - " - Backend - contains `Syft` and corresponding logic to execute code in _sync_ manner\n", - " - Backend Stream - contains `Syft` and logic to queue message in RabbitMQ\n", - " - Celery Worker - contains `Syft` and logic to execute message received from RabbitMQ\n", - " - RabbitMQ - receives messages from Backend Stream and passes them into Celery Worker\n", - " - Redis - each `syft` object has a `UUID`, and stored in Redis as a `key`/`value` pair\n", - " - Mongo - Stores non-private metadata that are related to `grid` operation, such as __RBAC__ or `BLOB`s metadata \n", - " - SeaweedFS - Stores the `BLOB`s, compatible with Amazon S3 protocols\n", - " - Jaeger - distributed end-to-end tracing\n", - "\n", - "__When you need this?__
\n", - "_When you need a Syft domain/gateway node locally._\n", - "\n", - "__Prerequistes:__
\n", - "1. Syft installed - [pypi.org/project/syft](https://pypi.org/project/syft/)\n", - "1. Hagrid installed - [pypi.org/project/syft](https://pypi.org/project/syft/)\n", - "1. Docker Installed - [docs.docker.com/get-docker](https://docs.docker.com/get-docker/)\n", - "\n", - "\n", - "Easiest way to launch the Full Container Stack is the `hagrid` cli tool.\n", - "\n", - "Basic syntax of Hagrdi deployment command is the following:
\n", - "> `hagrid launch to :`\n", - "\n", - "To deploy the full container stack use the following command:
\n", - "\n", - "> `hagrid launch test_domain domain to docker:8081`\n", - "\n", - "For detailed explanation of Full Container Stack deployment refer to the notebook [05-deploy-stack.ipynb](https://github.com/OpenMined/PySyft/blob/dev/notebooks/tutorials/data-engineer/05-deploy-stack.ipynb)" - ] - }, - { - "cell_type": "markdown", - "id": "22", - "metadata": {}, - "source": [ - "----" - ] - }, - { - "cell_type": "markdown", - "id": "23", - "metadata": {}, - "source": [ - "## VM Container Host" - ] - }, - { - "cell_type": "markdown", - "id": "24", - "metadata": {}, - "source": [ - "Ability to easily deploy `syft` stack to __anywhere__. By anywhere we mean an existing linux server accessible via `ssh` connection. `hagrid` cli tool can do all the hard work for us, by defining the desired system state using `ansible` and deploying all containers (defined in the previous section).\n", - "\n", - "__When you need this?__
\n", - "_When you need to deploy Syft domain/gateway node on a remote host, whether Virtual Machine or real Linux server._\n", - "\n", - "__Prerequistes:__
\n", - "1. Syft installed - [pypi.org/project/syft](https://pypi.org/project/syft/)\n", - "2. Hagrid installed - [pypi.org/project/syft](https://pypi.org/project/syft/)\n", - "3. VM accessible via SSH\n", - "\n", - "Deploy Syft `domain`/`network` node to the remote VM using following command:\n", - "\n", - "> `hagrid launch test_domain domain to 100.0.0.1 --username=ubuntu --auth-type=key --key-path=~/.ssh/hagrid_ssh_key`\n", - "\n", - "All flags marked with `--` are optional, if not provided `hagrid` will interactively ask you to provide all necessary details. More details on `hagrid` usage can be found in following notebook [03-hagrid.ipynb](https://github.com/OpenMined/PySyft/blob/dev/notebooks/tutorials/data-engineer/03-hagrid.ipynb)\n", - "\n", - "If you want to deploy to Cloud providers reffer to corresponding notebook:\n", - "- Azure - [06-deploy-to-azure.ipynb](https://github.com/OpenMined/PySyft/blob/dev/notebooks/tutorials/data-engineer/06-deploy-to-azure.ipynb)\n", - "- GCP - [07-deploy-to-gcp.ipynb](https://github.com/OpenMined/PySyft/blob/dev/notebooks/tutorials/data-engineer/07-deploy-to-gcp.ipynb)\n", - "- AWS - [08-deploy-to-aws.ipynb](https://github.com/OpenMined/PySyft/blob/dev/notebooks/tutorials/data-engineer/08-deploy-to-aws.ipynb)\n", - "\n", - ">__Note__: VM Container Host supports deployment _only from Linux or MacOS_ machines, since it requires `ansible`
that is not supported by Windows \n" - ] - }, - { - "cell_type": "markdown", - "id": "25", - "metadata": {}, - "source": [ - "----" - ] - }, - { - "cell_type": "markdown", - "id": "26", - "metadata": {}, - "source": [ - "## Gateway Nodes" - ] - }, - { - "cell_type": "markdown", - "id": "27", - "metadata": {}, - "source": [ - "Gateway Nodes are used to interconnect multiple `domain` nodes.\n", - "Essentially, `gateway` nodes use the same containers and code, although with different configurations.\n", - "`gateway` nodes do not have the Frontend and Blob storage. \n", - "\n", - "__When you need this?__
\n", - "_When you need to interconnect two or more domain nodes._\n", - "\n", - "__Prerequistes:__
\n", - "1. Syft installed - [pypi.org/project/syft](https://pypi.org/project/syft/)\n", - "1. Hagrid installed - [pypi.org/project/syft](https://pypi.org/project/syft/)\n", - "1. Docker installed or SSH connection to VM\n", - "\n", - "The `hagrid` cli can be used to deploy the `gateway` nodes, as a local container stack deployment or remote VM host deployment.\n", - "\n", - "To deploy `gateway` node us the following command:
\n", - "> `hagrid launch gateway to :`\n", - "\n", - "Example of launching the `gateway` node called `test-gateway`:
\n", - "> `hagrid launch test-gateway gateway to docker:9082`\n" - ] - }, - { - "cell_type": "markdown", - "id": "28", - "metadata": {}, - "source": [ - "----" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/03-hagrid.ipynb b/notebooks/tutorials/data-engineer/03-hagrid.ipynb deleted file mode 100644 index 3ad7cf9c25d..00000000000 --- a/notebooks/tutorials/data-engineer/03-hagrid.ipynb +++ /dev/null @@ -1,73 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# HAGrid" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## Installing HAGrid" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "## Python PATH" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "## Debugging HAGrid" - ] - }, - { - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "## Ansible and Windows" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/04-deploy-container.ipynb b/notebooks/tutorials/data-engineer/04-deploy-container.ipynb deleted file mode 100644 index dd016d74ae5..00000000000 --- a/notebooks/tutorials/data-engineer/04-deploy-container.ipynb +++ /dev/null @@ -1,107 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Deploying a Container" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## Docker 1-liner" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "```\n", - "$ docker run -it -e DEFAULT_ROOT_PASSWORD=secret -e PORT=8080 -p 8080:8080 openmined/grid-enclave:0.8.2.b0\n", - "```" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "## Azure CLI" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "$ az group create --name test-container --location eastus" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "$ az container create --resource-group test-container --name syft --image openmined/grid-enclave:0.8.2.b0 --dns-name-label syft-demo --ports 80 --environment-variables PORT=80 DEFAULT_ROOT_PASSWORD=secret" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "6", - "metadata": {}, - "source": [ - "## From HAGrid" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7", - "metadata": {}, - "source": [ - "## Volume Mounts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/05-deploy-stack.ipynb b/notebooks/tutorials/data-engineer/05-deploy-stack.ipynb deleted file mode 100644 index 2ac0fcc7dff..00000000000 --- a/notebooks/tutorials/data-engineer/05-deploy-stack.ipynb +++ /dev/null @@ -1,81 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Deploy the Stack" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## Docker Compose" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "## HAGrid" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "## Build Source" - ] - }, - { - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "## Volume Mounts" - ] - }, - { - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "## Docker Networks" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/06-deploy-to-azure.ipynb b/notebooks/tutorials/data-engineer/06-deploy-to-azure.ipynb deleted file mode 100644 index 397d3f1016b..00000000000 --- a/notebooks/tutorials/data-engineer/06-deploy-to-azure.ipynb +++ /dev/null @@ -1,114 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Deploy to Azure" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## Installing CLI Tool" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "## Authorizing CLI Tool" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "## Deploying a Single Container" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "$ az group create --name test-container --location eastus" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "$ az container create --resource-group test-container --name syft --image openmined/grid-enclave:0.8.2.b0 --dns-name-label syft-demo --ports 80 --environment-variables PORT=80 DEFAULT_ROOT_PASSWORD=secret" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "6", - "metadata": {}, - "source": [ - "## Deploying a Domain" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7", - "metadata": {}, - "source": [ - "## Checking Firewall Rules" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "8", - "metadata": {}, - "source": [ - "## Logging in via SSH" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/07-deploy-to-gcp.ipynb b/notebooks/tutorials/data-engineer/07-deploy-to-gcp.ipynb deleted file mode 100644 index 827f1d5e129..00000000000 --- a/notebooks/tutorials/data-engineer/07-deploy-to-gcp.ipynb +++ /dev/null @@ -1,73 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Deploy to Google Cloud Platform (GCP)" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## Installing CLI Tool" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "## Authorizing CLI Tool" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "## Deploying a Domain" - ] - }, - { - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "## Checking Firewall Rules" - ] - }, - { - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "## Logging in via SSH" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/08-deploy-to-aws.ipynb b/notebooks/tutorials/data-engineer/08-deploy-to-aws.ipynb deleted file mode 100644 index 7b8a28ec777..00000000000 --- a/notebooks/tutorials/data-engineer/08-deploy-to-aws.ipynb +++ /dev/null @@ -1,152 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Deploy to AWS" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## Installing CLI Tool" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "Please refer to the docs for installing the AWS CLI tool: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html. It has instructions for the different operating systems such as Mac, Windows and Linux" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "## Authorizing CLI Tool" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "Please go through this for setting up the CLI: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html. \n", - "\n", - "A common/quick way is to use to authenticate using IAM user credentials. Please refer to this doc for the steps involved: https://docs.aws.amazon.com/cli/latest/userguide/cli-authentication-user.html" - ] - }, - { - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "## Deploying a Domain" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "6", - "metadata": {}, - "source": [ - "Use `hagrid launch {domain_name} domain to aws [--no-provision]` command to launch your domain to an AWS EC2 instance. The --no-provision flag is optional and can be used if you do not want to provision all the resources using ansible (If you're not familiar with this, just ignore this flag) " - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7", - "metadata": {}, - "source": [ - "You would be prompted with a series of questions.\n", - "\n", - "Please specify the region where you want your EC2 instance to be deployed.\n", - "\n", - "Please specify a name for the security group to be created. A security group is used to control the inbound and outbound traffic to/from the EC2 instance. Please check https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-security-groups.html for more information.\n", - "Then specify the IP addresses to be white-listed for incoming traffic to the EC2 instance. Please ensure that you enter it in CIDR notation. The default is 0.0.0.0/0 which means that all inbound traffic is allowed.\n", - "On these IP addresses, we open the following ports: 80, 443, 22.\n", - "\n", - "Then, please specify the EC2 instance type. By default, it is t2.xlarge.\n", - "\n", - "We need an EC2 key pair in order to SSH into the instance. If you already have a key-pair, please specify the name and the path where it is stored. Otherwise, if you do not have one, we will create one with the given name and store it in the path you specify. (Note: creating a keypair might not work properly with windows powershell).\n", - "\n", - "\n", - "Then specify the repo and branch for the source code. You can leave it as the default.\n", - "\n", - "\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "8", - "metadata": {}, - "source": [ - "## Checking Firewall Rules" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "9", - "metadata": {}, - "source": [ - "You could go to the AWS console, and navigate to the region where you deployed your instance. Search for EC2 and go over to the Security Groups tab (or directly search for Security Group). In the list of security groups, identify the one you created using the name. If you go inside, you would see the inbound and outbound rules." - ] - }, - { - "cell_type": "markdown", - "id": "10", - "metadata": {}, - "source": [ - "## Logging in via SSH" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "11", - "metadata": {}, - "source": [ - "Please refer to the steps in the doc to connect to your EC2 instance using SSH: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html" - ] - }, - { - "cell_type": "markdown", - "id": "12", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/09-deploying-enclave.ipynb b/notebooks/tutorials/data-engineer/09-deploying-enclave.ipynb deleted file mode 100644 index 11c0fba438e..00000000000 --- a/notebooks/tutorials/data-engineer/09-deploying-enclave.ipynb +++ /dev/null @@ -1,41 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Deploying an Enclave" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/10-custom-deployment.ipynb b/notebooks/tutorials/data-engineer/10-custom-deployment.ipynb deleted file mode 100644 index 11b2f707b35..00000000000 --- a/notebooks/tutorials/data-engineer/10-custom-deployment.ipynb +++ /dev/null @@ -1,97 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Custom Deployment" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## What you need" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "### Container Engine" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "### File Mounts" - ] - }, - { - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "### Network Access" - ] - }, - { - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "### Python Client" - ] - }, - { - "cell_type": "markdown", - "id": "6", - "metadata": {}, - "source": [ - "### Red Hat and Podman" - ] - }, - { - "cell_type": "markdown", - "id": "7", - "metadata": {}, - "source": [ - "### Kubernetes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-engineer/11-installing-and-upgrading-via-helm.ipynb b/notebooks/tutorials/data-engineer/11-installing-and-upgrading-via-helm.ipynb deleted file mode 100644 index 4775672f760..00000000000 --- a/notebooks/tutorials/data-engineer/11-installing-and-upgrading-via-helm.ipynb +++ /dev/null @@ -1,364 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Installing using Helm" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## Add Helm Repo" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "```bash\n", - "helm repo add openmined https://openmined.github.io/PySyft/helm\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "## Update Repo" - ] - }, - { - "cell_type": "markdown", - "id": "4", - "metadata": {}, - "source": [ - "```bash\n", - "helm repo update openmined\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "## Search for available Chart versions" - ] - }, - { - "cell_type": "markdown", - "id": "6", - "metadata": {}, - "source": [ - "### Search for available versionsΒΆ" - ] - }, - { - "cell_type": "markdown", - "id": "7", - "metadata": {}, - "source": [ - "```bash\n", - "helm search repo openmined/syft --versions --devel\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "8", - "metadata": {}, - "source": [ - "### Set the version to install" - ] - }, - { - "cell_type": "markdown", - "id": "9", - "metadata": {}, - "source": [ - "```bash\n", - "export SYFT_VERSION=\"\"\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "10", - "metadata": {}, - "source": [ - "## Setup a registry" - ] - }, - { - "cell_type": "markdown", - "id": "11", - "metadata": {}, - "source": [ - "One needs to setup a registry either locally or on the cloud. To set one up locally, one can follow the following commands." - ] - }, - { - "cell_type": "markdown", - "id": "12", - "metadata": {}, - "source": [ - "```bash\n", - "k3d registry create registry.localhost --port 12345 -v `pwd`/k3d-registry:/var/lib/registry || true\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "13", - "metadata": {}, - "source": [ - "Setup a load balancer\n", - "\n", - "```bash\n", - "NODE_NAME=syft NODE_PORT=8080 && \\\n", - "k3d cluster create syft -p \"$NODE_PORT:80@loadbalancer\" --registry-use k3d-registry.localhost || true \\\n", - "k3d cluster start syft\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "14", - "metadata": {}, - "source": [ - "## Install using Helm" - ] - }, - { - "cell_type": "markdown", - "id": "15", - "metadata": {}, - "source": [ - "```bash\n", - "helm install my-domain openmined/syft --version $SYFT_VERSION --namespace syft --create-namespace --set ingress.className=traefik\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "16", - "metadata": {}, - "source": [ - "# Upgrading using Helm" - ] - }, - { - "cell_type": "markdown", - "id": "17", - "metadata": {}, - "source": [ - "## Add Helm Repo" - ] - }, - { - "cell_type": "markdown", - "id": "18", - "metadata": {}, - "source": [ - "```bash\n", - "helm repo add openmined https://openmined.github.io/PySyft/helm\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "19", - "metadata": {}, - "source": [ - "## Update Repo" - ] - }, - { - "cell_type": "markdown", - "id": "20", - "metadata": {}, - "source": [ - "```bash\n", - "helm repo update openmined\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "21", - "metadata": {}, - "source": [ - "## Search for available Helm Chart versions" - ] - }, - { - "cell_type": "markdown", - "id": "22", - "metadata": {}, - "source": [ - "### Search for available versions" - ] - }, - { - "cell_type": "markdown", - "id": "23", - "metadata": {}, - "source": [ - "```bash\n", - "helm search repo openmined/syft --versions --devel\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "24", - "metadata": {}, - "source": [ - "### Set the target version" - ] - }, - { - "cell_type": "markdown", - "id": "25", - "metadata": {}, - "source": [ - "```bash\n", - "export TARGET_VERSION=\"\"\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "26", - "metadata": {}, - "source": [ - "## Get the current Helm release values (User Defined)" - ] - }, - { - "cell_type": "markdown", - "id": "27", - "metadata": {}, - "source": [ - "Set the release name and namespace\n", - "\n", - "```bash\n", - "export RELEASE_NAME=\"\"\n", - "export NAMESPACE=\"\"\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "28", - "metadata": {}, - "source": [ - "```bash\n", - "helm get values $RELEASE_NAME -n $NAMESPACE -o yaml > values.yaml\n", - "```\n", - "\n", - "
\n", - "\n", - "Use this file in the argument to helm upgrade command, for example:\n", - "\n", - "\n", - "`-f /home/user/values.yaml`\n", - "\n", - "\n", - "Save the path to a variable:\n", - "\n", - "```bash\n", - "export PATH_TO_VALUES=/home/user/values.yaml\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "29", - "metadata": {}, - "source": [ - "## Upgrade the Helm Chart" - ] - }, - { - "cell_type": "markdown", - "id": "30", - "metadata": {}, - "source": [ - "### Find out the number of nodes in the cluster." - ] - }, - { - "cell_type": "markdown", - "id": "31", - "metadata": {}, - "source": [ - "```bash\n", - "kubectl describe sts --namespace $NAMESPACE | grep 'Replicas'\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "32", - "metadata": {}, - "source": [ - "### Upgrade the Helm chart." - ] - }, - { - "cell_type": "markdown", - "id": "33", - "metadata": {}, - "source": [ - "```bash\n", - "helm upgrade $RELEASE_NAME openmined/syft \\\n", - " --version $TARGET_VERSION \\\n", - " -f $PATH_TO_VALUES \\\n", - " --namespace $NAMESPACE\n", - "```" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/tutorials/data-owner/03-messages-and-requests.ipynb b/notebooks/tutorials/data-owner/03-messages-and-requests.ipynb index 5a59e9724f0..8e7a1618425 100644 --- a/notebooks/tutorials/data-owner/03-messages-and-requests.ipynb +++ b/notebooks/tutorials/data-owner/03-messages-and-requests.ipynb @@ -200,7 +200,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "project" ] }, diff --git a/notebooks/tutorials/data-scientist/05-syft-functions.ipynb b/notebooks/tutorials/data-scientist/05-syft-functions.ipynb index da524a933e1..cbee1755a3d 100644 --- a/notebooks/tutorials/data-scientist/05-syft-functions.ipynb +++ b/notebooks/tutorials/data-scientist/05-syft-functions.ipynb @@ -400,7 +400,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "project" ] }, diff --git a/notebooks/tutorials/data-scientist/06-messaging-and-requests.ipynb b/notebooks/tutorials/data-scientist/06-messaging-and-requests.ipynb index 3fbe3bfc055..5d7ff62fa94 100644 --- a/notebooks/tutorials/data-scientist/06-messaging-and-requests.ipynb +++ b/notebooks/tutorials/data-scientist/06-messaging-and-requests.ipynb @@ -200,7 +200,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "project" ] }, diff --git a/notebooks/tutorials/deployments/00-deployment-types.ipynb b/notebooks/tutorials/deployments/00-deployment-types.ipynb new file mode 100644 index 00000000000..b9283a0c94c --- /dev/null +++ b/notebooks/tutorials/deployments/00-deployment-types.ipynb @@ -0,0 +1,99 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction to PySyft Deployment Options\n", + "\n", + "PySyft offers various deployment options catering to different needs and environments. Each deployment option provides a unique set of advantages, allowing users to seamlessly integrate PySyft into their workflows, whether for local development, production deployment, or experimentation in cloud environments. Below, we explore the different deployment options supported by PySyft and provide insights into when each option is most suitable." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. Local Python Deployment\n", + "\n", + "This deployment option runs PySyft locally within a Python environment. It is lightweight and runs everything in-memory, making it ideal for quick prototyping and testing.\n", + "\n", + "**Recommended For:** \n", + "- Development and testing on resource-constrained systems without Docker support.\n", + "- Rapid experimentation with PySyft APIs.\n", + "\n", + "Follow [01-deploy-python.ipynb](./01-deploy-python.ipynb) for deployment instructions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Single Container Deployment\n", + "\n", + "In this deployment, PySyft is encapsulated within a single Docker container, providing better isolation and portability compared to the local Python deployment.\n", + "\n", + "**Recommended For:**\n", + "- Resource-constrained systems with Docker support.\n", + "- Standardizing PySyft deployment across different environments.\n", + "\n", + "Follow [02-deploy-container.ipynb](./02-deploy-container.ipynb) for deployment instructions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Kubernetes Deployment\n", + "\n", + "This deployment option orchestrates the entire PySyft stack on a Kubernetes cluster, enabling scalable and efficient deployment in cloud or on-premises environments. Various Kubernetes configurations are available for deployment flexibility.\n", + "\n", + "**Recommended For:**\n", + "- Production-grade deployments requiring scalability and fault tolerance.\n", + "- Cloud-native environments where Kubernetes is the preferred orchestration tool.\n", + "\n", + " **[a. Local k3d Cluster Deployment](./03-deploy-k8s-k3d.ipynb)**\n", + " - Quick setup for local development and testing using a lightweight Kubernetes cluster.\n", + "\n", + " **[b. Azure Deployment](./04-deploy-k8s-azure.ipynb)**\n", + " - Deployment on Microsoft Azure cloud infrastructure for scalable and reliable operation.\n", + "\n", + " **[c. GCP Deployment](./05-deploy-k8s-gcp.ipynb)**\n", + " - Deployment on Google Cloud Platform for seamless integration with GCP services.\n", + "\n", + " **[d. AWS Deployment](./06-deploy-k8s-aws.ipynb)**\n", + " - Deployment on Amazon Web Services for robust and flexible cloud-based deployment." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4. Devspace Deployment\n", + "\n", + "This deployment option utilizes Devspace to streamline the development process for PySyft. It provides features such as local image building, port-forwarding, volume mounting, hot-reloading, and debugging to enhance the development experience.\n", + "\n", + "**Recommended For:**\n", + "- Developers contributing to PySyft codebase.\n", + "- Simplifying local development setup and debugging processes.\n", + "\n", + "Follow [07-deploy-devspace.ipynb](./07-deploy-devspace.ipynb) for deployment instructions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Choosing the Right Deployment Option\n", + "\n", + "Selecting the appropriate deployment option depends on factors such as development objectives, resource constraints, scalability requirements, and familiarity with the deployment environment. For quick experimentation and local development, the local Python deployment or single container deployment may suffice. However, for production-grade deployments requiring scalability and reliability, Kubernetes deployment is recommended. Developers actively contributing to PySyft can benefit from the Devspace deployment option for a streamlined development experience." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/tutorials/deployments/01-deploy-python.ipynb b/notebooks/tutorials/deployments/01-deploy-python.ipynb new file mode 100644 index 00000000000..b0de2c53fa3 --- /dev/null +++ b/notebooks/tutorials/deployments/01-deploy-python.ipynb @@ -0,0 +1,191 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Local in-memory python deployment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "One of the quickest way to try out PySyft is to install the pre-built python package on your local environment using pip. The python package is lightweight and runs the PySyft stack in-memory.\n", + "\n", + "**Recommended For:**\n", + "- Development and testing on resource-constrained systems without Docker support.\n", + "- Rapid experimentation with PySyft APIs." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "Before we begin, ensure you have the following prerequisites installed on your system:\n", + "1. Python (3.10 - 3.12)\n", + "2. pip (or uv)\n", + "3. venv (optional, but recommended)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Deployment Steps" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Installing Syft\n", + "1. Create and activate a python virtual environment (Optional, but recommended)\n", + " ```bash\n", + " python -m venv venv/\n", + " source venv/bin/activate\n", + " ```\n", + "\n", + "2. Install PySyft\n", + " ```bash\n", + " pip install syft\n", + " ```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Working with Python Domain\n", + "\n", + "`PySyft` makes it very easy to develop against a domain in a notebook by providing the `sy.orchestra` interface. It allows you to start a domain with a webserver in a notebook in the background, which is a lightweight version of a Domain that would be used in production. You can specify options such as what kind of database you are using, whether you want to use networking and how many processes you want to use. You can launch a Domain by simply executing:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# syft absolute\n", + "import syft as sy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "node = sy.orchestra.launch(\n", + " name=\"dev-mode-example-domain-1\", port=8020, reset=True, dev_mode=True\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we don't need a webserver (for development this is true in many cases), we can omit the port and instead use\n", + "\n", + "```python\n", + "node = sy.orchestra.launch(name=\"dev-mode-example-domain-1\", dev_mode=True, reset=True)\n", + "```\n", + "\n", + "One of the benefits of not using a port is that you can use a debugger and set breakpoints within api calls. This makes debugging way faster in many cases.\n", + "\n", + "Now, we are ready to start using the domain. The domain comes with test login credentials for the admin." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client = node.login(email=\"info@openmined.org\", password=\"changethis\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once you are logged in, you are ready to start using the domain, for instance for creating a dataset (this one is empty, just as a example)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = sy.Dataset(name=\"my dataset\", asset_list=[])\n", + "client.upload_dataset(dataset)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly to stop or terminate your Domain, we can execute the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "node.land()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next Steps\n", + "Congratulations! You have successfully deployed a local in-memory PySyft stack using python. Now, you can explore its capabilities and use cases through our API example notebooks:\n", + "\n", + "πŸ“ [API Example Notebooks](../../api)\n", + "- [00-load-data.ipynb](../../api/0.8/00-load-data.ipynb)\n", + "- [01-submit-code.ipynb](../../api/0.8/01-submit-code.ipynb)\n", + "- [02-review-code-and-approve.ipynb](../../api/0.8/02-review-code-and-approve.ipynb)\n", + "- [03-data-scientist-download-result.ipynb](../../api/0.8/03-data-scientist-download-result.ipynb)\n", + "- [04-jax-example.ipynb](../../api/0.8/04-jax-example.ipynb)\n", + "- [05-custom-policy.ipynb](../../api/0.8/05-custom-policy.ipynb)\n", + "- [06-multiple-code-requests.ipynb](../../api/0.8/06-multiple-code-requests.ipynb)\n", + "- [07-domain-register-control-flow.ipynb](../../api/0.8/07-domain-register-control-flow.ipynb)\n", + "- [08-code-version.ipynb](../../api/0.8/08-code-version.ipynb)\n", + "- [09-blob-storage.ipynb](../../api/0.8/09-blob-storage.ipynb)\n", + "- [10-container-images.ipynb](../../api/0.8/10-container-images.ipynb)\n", + "- [11-container-images-k8s.ipynb](../../api/0.8/11-container-images-k8s.ipynb)\n", + "\n", + "Feel free to explore these notebooks to get started with PySyft and unlock its full potential for privacy-preserving machine learning!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "PySyft", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/tutorials/deployments/02-deploy-container.ipynb b/notebooks/tutorials/deployments/02-deploy-container.ipynb new file mode 100644 index 00000000000..be1c530ab2b --- /dev/null +++ b/notebooks/tutorials/deployments/02-deploy-container.ipynb @@ -0,0 +1,171 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Single container deployment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## Introduction\n", + "\n", + "In this deployment, PySyft is encapsulated within a single Docker container, providing better isolation and portability compared to the local Python deployment.\n", + "\n", + "**Recommended For:**\n", + "- Resource-constrained systems with Docker support.\n", + "- Standardizing PySyft deployment across different environments." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "Before we begin, ensure you have [Docker](https://docs.docker.com/install/) installed on your system." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Deployment steps\n", + "\n", + "You can execute the below command in your terminal to run the PySyft stack within a single docker container on port `8080`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Set Your Preferred Syft Version\n", + "\n", + "```sh\n", + "SYFT_VERSION=\"\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "``` bash\n", + "docker run -it \\\n", + " -e NODE_NAME=syft-example-domain-1 \\\n", + " -e NODE_TYPE=domain \\\n", + " -e N_CONSUMERS=1 \\\n", + " -e SINGLE_CONTAINER_MODE=true \\\n", + " -e CREATE_PRODUCER=true \\\n", + " -e INMEMORY_WORKERS=true \\\n", + " -p 8080:80 --add-host=host.docker.internal:host-gateway \\\n", + " --name syft-example-domain-1 openmined/grid-backend:$SYFT_VERSION\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Working with the single container deployment\n", + "\n", + "PySyft makes it very simple to connect to any existing Syft cluster by providing the `sy.orchestra` interface. You can connect to the domain by executing these steps in your jupyter notebook:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python3\n", + "# syft absolute\n", + "import syft as sy\n", + "\n", + "node = sy.orchestra.launch(name=\"syft-example-domain-1\", deploy_to=\"remote\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This will return a node handle by connecting to `http://localhost:8080` which is the default host and port where your docker container will be running. You can connect to a different host and port by setting the environment variables `NODE_URL` and `NODE_PORT`.\n", + "```python\n", + "import os\n", + "\n", + "os.environ[\"NODE_URL\"] = \"\"\n", + "os.environ[\"NODE_PORT\"] = \"\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we are ready to start using the domain. The domain comes with default login credentials for the admin." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python3\n", + "client = node.login(email=\"info@openmined.org\", password=\"changethis\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once you are logged in, you are ready to start using the domain, for instance for creating a dataset (this one is empty, just as a example)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python3\n", + "dataset = sy.Dataset(name=\"my dataset\", asset_list=[])\n", + "client.upload_dataset(dataset)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next Steps\n", + "Congratulations! You have successfully deployed PySyft on your local Kubernetes cluster. Now, you can explore its capabilities and use cases through our API example notebooks:\n", + "\n", + "πŸ“ [API Example Notebooks](../../api)\n", + "- [00-load-data.ipynb](../../api/0.8/00-load-data.ipynb)\n", + "- [01-submit-code.ipynb](../../api/0.8/01-submit-code.ipynb)\n", + "- [02-review-code-and-approve.ipynb](../../api/0.8/02-review-code-and-approve.ipynb)\n", + "- [03-data-scientist-download-result.ipynb](../../api/0.8/03-data-scientist-download-result.ipynb)\n", + "- [04-jax-example.ipynb](../../api/0.8/04-jax-example.ipynb)\n", + "- [05-custom-policy.ipynb](../../api/0.8/05-custom-policy.ipynb)\n", + "- [06-multiple-code-requests.ipynb](../../api/0.8/06-multiple-code-requests.ipynb)\n", + "- [07-domain-register-control-flow.ipynb](../../api/0.8/07-domain-register-control-flow.ipynb)\n", + "- [08-code-version.ipynb](../../api/0.8/08-code-version.ipynb)\n", + "- [09-blob-storage.ipynb](../../api/0.8/09-blob-storage.ipynb)\n", + "- [10-container-images.ipynb](../../api/0.8/10-container-images.ipynb)\n", + "- [11-container-images-k8s.ipynb](../../api/0.8/11-container-images-k8s.ipynb)\n", + "\n", + "Feel free to explore these notebooks to get started with PySyft and unlock its full potential for privacy-preserving machine learning!" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/tutorials/deployments/03-deploy-k8s-k3d.ipynb b/notebooks/tutorials/deployments/03-deploy-k8s-k3d.ipynb new file mode 100644 index 00000000000..c9cb2e1ebd8 --- /dev/null +++ b/notebooks/tutorials/deployments/03-deploy-k8s-k3d.ipynb @@ -0,0 +1,193 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Deploying PySyft on a Local Kubernetes Cluster" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## Introduction\n", + "Welcome to our quick start guide for deploying PySyft on a local Kubernetes cluster! PySyft is a powerful framework for privacy-preserving machine learning, and deploying it on Kubernetes allows an easy way to quickly try out the full PySyft stack on your own system. This guide will walk you through the process step by step." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## Prerequisites\n", + "Before we begin, ensure you have the following prerequisites installed on your system:\n", + "1. [Docker](https://docs.docker.com/install/): Docker is required to create and manage containers.\n", + "2. [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl): kubectl is the command-line tool for interacting with Kubernetes clusters.\n", + "3. [k3d](https://k3d.io/v5.6.3/#installation): k3d is used to create local Kubernetes clusters.\n", + "4. [Helm](https://helm.sh/docs/intro/install/): Helm is the package manager for Kubernetes, used to install and manage applications on Kubernetes clusters." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Deployment Steps\n", + "\n", + "### 1. Create a Local Kubernetes Cluster\n", + "First, create a local Kubernetes cluster named \"syft\" using k3d:\n", + "```sh\n", + "k3d cluster create syft -p \"8080:80@loadbalancer\"\n", + "```\n", + "\n", + "### 2. Add and Update Helm Repo for Syft\n", + "Add the Helm repository for PySyft and update it:\n", + "```sh\n", + "helm repo add openmined https://openmined.github.io/PySyft/helm\n", + "helm repo update openmined\n", + "```\n", + "\n", + "### 3. Search for Available Syft Versions\n", + "Explore available versions of PySyft using Helm:\n", + "```sh\n", + "helm search repo openmined/syft --versions --devel\n", + "```\n", + "\n", + "### 4. Set Your Preferred Syft Chart Version\n", + "Set the version of PySyft you want to install:\n", + "```sh\n", + "SYFT_VERSION=\"\"\n", + "```\n", + "\n", + "### 5. Provision Helm Charts\n", + "Install PySyft on the Kubernetes cluster with your preferred version:\n", + "```sh\n", + "helm install my-syft openmined/syft --version $SYFT_VERSION --namespace syft --create-namespace --set ingress.className=\"traefik\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## Working with the local Kubernetes deployment\n", + "\n", + "PySyft makes it very simple to connect to your existing Syft cluster by providing the `sy.orchestra` interface. You can connect to the domain by executing these steps in your jupyter notebook:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python3\n", + "# syft absolute\n", + "import syft as sy\n", + "\n", + "node = sy.orchestra.launch(name=\"syft-example-domain-1\", deploy_to=\"remote\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This will return a node handle by connecting to `http://localhost:8080` which is the default host and port where your kubernetes cluster will be running. You can connect to a different host and port by setting the environment variables `NODE_URL` and `NODE_PORT`.\n", + "```python\n", + "import os\n", + "\n", + "os.environ[\"NODE_URL\"] = \"\"\n", + "os.environ[\"NODE_PORT\"] = \"\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we are ready to start using the domain. The domain comes with default login credentials for the admin." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python3\n", + "client = node.login(email=\"info@openmined.org\", password=\"changethis\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once you are logged in, you are ready to start using the domain, for instance for creating a dataset (this one is empty, just as a example)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python3\n", + "dataset = sy.Dataset(name=\"my dataset\", asset_list=[])\n", + "client.upload_dataset(dataset)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next Steps\n", + "Congratulations! You have successfully deployed PySyft on your local Kubernetes cluster. Now, you can explore its capabilities and use cases through our API example notebooks:\n", + "\n", + "πŸ“ [API Example Notebooks](../../api)\n", + "- [00-load-data.ipynb](../../api/0.8/00-load-data.ipynb)\n", + "- [01-submit-code.ipynb](../../api/0.8/01-submit-code.ipynb)\n", + "- [02-review-code-and-approve.ipynb](../../api/0.8/02-review-code-and-approve.ipynb)\n", + "- [03-data-scientist-download-result.ipynb](../../api/0.8/03-data-scientist-download-result.ipynb)\n", + "- [04-jax-example.ipynb](../../api/0.8/04-jax-example.ipynb)\n", + "- [05-custom-policy.ipynb](../../api/0.8/05-custom-policy.ipynb)\n", + "- [06-multiple-code-requests.ipynb](../../api/0.8/06-multiple-code-requests.ipynb)\n", + "- [07-domain-register-control-flow.ipynb](../../api/0.8/07-domain-register-control-flow.ipynb)\n", + "- [08-code-version.ipynb](../../api/0.8/08-code-version.ipynb)\n", + "- [09-blob-storage.ipynb](../../api/0.8/09-blob-storage.ipynb)\n", + "- [10-container-images.ipynb](../../api/0.8/10-container-images.ipynb)\n", + "- [11-container-images-k8s.ipynb](../../api/0.8/11-container-images-k8s.ipynb)\n", + "\n", + "Feel free to explore these notebooks to get started with PySyft and unlock its full potential for privacy-preserving machine learning!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "PySyft", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/tutorials/deployments/04-deploy-k8s-azure.ipynb b/notebooks/tutorials/deployments/04-deploy-k8s-azure.ipynb new file mode 100644 index 00000000000..71c158afddd --- /dev/null +++ b/notebooks/tutorials/deployments/04-deploy-k8s-azure.ipynb @@ -0,0 +1,18 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/tutorials/deployments/05-deploy-k8s-gcp.ipynb b/notebooks/tutorials/deployments/05-deploy-k8s-gcp.ipynb new file mode 100644 index 00000000000..71c158afddd --- /dev/null +++ b/notebooks/tutorials/deployments/05-deploy-k8s-gcp.ipynb @@ -0,0 +1,18 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/tutorials/deployments/06-deploy-k8s-aws.ipynb b/notebooks/tutorials/deployments/06-deploy-k8s-aws.ipynb new file mode 100644 index 00000000000..71c158afddd --- /dev/null +++ b/notebooks/tutorials/deployments/06-deploy-k8s-aws.ipynb @@ -0,0 +1,18 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/tutorials/deployments/07-deploy-devspace.ipynb b/notebooks/tutorials/deployments/07-deploy-devspace.ipynb new file mode 100644 index 00000000000..71c158afddd --- /dev/null +++ b/notebooks/tutorials/deployments/07-deploy-devspace.ipynb @@ -0,0 +1,18 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/tutorials/enclaves/Enclave-single-notebook-high-low-network.ipynb b/notebooks/tutorials/enclaves/Enclave-single-notebook-high-low-network.ipynb index ecfdefff737..771f0ad4389 100644 --- a/notebooks/tutorials/enclaves/Enclave-single-notebook-high-low-network.ipynb +++ b/notebooks/tutorials/enclaves/Enclave-single-notebook-high-low-network.ipynb @@ -45,7 +45,7 @@ "metadata": {}, "outputs": [], "source": [ - "embassador_node_low = sy.Orchestra.launch(\n", + "embassador_node_low = sy.orchestra.launch(\n", " name=\"ambassador node\",\n", " node_side_type=\"low\",\n", " local_db=True,\n", @@ -69,14 +69,14 @@ "metadata": {}, "outputs": [], "source": [ - "ca_node_low = sy.Orchestra.launch(\n", + "ca_node_low = sy.orchestra.launch(\n", " name=\"canada-1\",\n", " node_side_type=\"low\",\n", " local_db=True,\n", " reset=True,\n", " # enable_warnings=True,\n", ")\n", - "it_node_low = sy.Orchestra.launch(\n", + "it_node_low = sy.orchestra.launch(\n", " name=\"italy-1\",\n", " node_side_type=\"low\",\n", " local_db=True,\n", @@ -125,13 +125,13 @@ " reset=True,\n", " # enable_warnings=True,\n", ")\n", - "ca_node_high = sy.Orchestra.launch(\n", + "ca_node_high = sy.orchestra.launch(\n", " name=\"canada-2\",\n", " local_db=True,\n", " reset=True,\n", " # enable_warnings=True,\n", ")\n", - "it_node_high = sy.Orchestra.launch(\n", + "it_node_high = sy.orchestra.launch(\n", " name=\"italy-2\",\n", " local_db=True,\n", " reset=True,\n", diff --git a/notebooks/tutorials/hello-syft/01-hello-syft.ipynb b/notebooks/tutorials/hello-syft/01-hello-syft.ipynb index b7354b469b1..8a7f6a674d2 100644 --- a/notebooks/tutorials/hello-syft/01-hello-syft.ipynb +++ b/notebooks/tutorials/hello-syft/01-hello-syft.ipynb @@ -518,6 +518,55 @@ "cell_type": "markdown", "id": "48", "metadata": {}, + "source": [ + "## Final note: autocomplete" + ] + }, + { + "cell_type": "markdown", + "id": "49", + "metadata": {}, + "source": [ + "Earlier in this tutorial, we used services defined on the client, such as `ds_client.code.request_code_execution`. To find out more about the available methods, like `.request_code_execution()`, and services, like `client.code` you can use autocomplete, simply type `ds_client.code.` or `ds_client.services.` for an example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "# autocompletion, but programtic. To test it out, just type client.services. instead in a new cell\n", + "autocompleter = get_ipython().Completer\n", + "_, completions1 = autocompleter.complete(text=\"ds_client.code.\")\n", + "_, completions2 = autocompleter.complete(text=\"ds_client.services.\")\n", + "_, completions3 = autocompleter.complete(text=\"ds_client.api.services.\")\n", + "_, completions4 = autocompleter.complete(text=\"ds_client.api.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "assert all(\n", + " [\n", + " \"ds_client.code.get_all\" in completions1,\n", + " \"ds_client.services.code\" in completions2,\n", + " \"ds_client.api.services.code\" in completions3,\n", + " \"ds_client.api.code\" in completions4,\n", + " \"ds_client.api.parse_raw\" not in completions4, # no pydantic completions on api\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, "source": [ "Once you are done with this tutorial, you can safely shut down the servers as following," ] @@ -525,7 +574,7 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "53", "metadata": {}, "outputs": [], "source": [ @@ -535,7 +584,7 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "54", "metadata": {}, "outputs": [], "source": [] @@ -557,7 +606,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.12.2" }, "toc": { "base_numbering": 1, diff --git a/notebooks/tutorials/model-auditing/colab/01-user-log.ipynb b/notebooks/tutorials/model-auditing/colab/01-user-log.ipynb index f53c1374203..036c21f9ed6 100644 --- a/notebooks/tutorials/model-auditing/colab/01-user-log.ipynb +++ b/notebooks/tutorials/model-auditing/colab/01-user-log.ipynb @@ -110,7 +110,7 @@ " \n", "**C) From the command line (supports docker/kubernetes)**\n", " - setup for production\n", - " - run `syft launch` or `hagrid launch` from the terminal\n", + " - run `syft launch` from the terminal\n", " \n", " \n", "We are using the **A)** here, as it is the only option available using google colab, switching to a real webserver is as easy as running this notebook in jupyter locally and adding a port. Read more about deployment on our [README.md](https://github.com/OpenMined/PySyft) and other setups for syft [here](https://github.com/OpenMined/PySyft/tree/dev/notebooks/tutorials/data-engineer)" @@ -481,7 +481,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = audit_project.start()\n", + "project = audit_project.send()\n", "project" ] }, diff --git a/notebooks/tutorials/model-training/01-data-scientist-submit-code.ipynb b/notebooks/tutorials/model-training/01-data-scientist-submit-code.ipynb index 4d245cd6f06..3314a8d70eb 100644 --- a/notebooks/tutorials/model-training/01-data-scientist-submit-code.ipynb +++ b/notebooks/tutorials/model-training/01-data-scientist-submit-code.ipynb @@ -492,7 +492,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = new_project.start()" + "project = new_project.send()" ] }, { diff --git a/notebooks/tutorials/pandas-cookbook/01-reading-from-a-csv.ipynb b/notebooks/tutorials/pandas-cookbook/01-reading-from-a-csv.ipynb index 730391a5881..d5cdc94cc9d 100644 --- a/notebooks/tutorials/pandas-cookbook/01-reading-from-a-csv.ipynb +++ b/notebooks/tutorials/pandas-cookbook/01-reading-from-a-csv.ipynb @@ -554,7 +554,7 @@ }, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "assert isinstance(project, sy.service.project.project.Project)\n", "project" ] diff --git a/notebooks/tutorials/pandas-cookbook/02-selecting-data-finding-common-complain.ipynb b/notebooks/tutorials/pandas-cookbook/02-selecting-data-finding-common-complain.ipynb index 28587a7e3d4..09e1e25b8dc 100644 --- a/notebooks/tutorials/pandas-cookbook/02-selecting-data-finding-common-complain.ipynb +++ b/notebooks/tutorials/pandas-cookbook/02-selecting-data-finding-common-complain.ipynb @@ -760,7 +760,7 @@ }, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "assert isinstance(project, sy.service.project.project.Project)\n", "project" ] diff --git a/notebooks/tutorials/pandas-cookbook/03-which-borough-has-the-most-noise-complaints.ipynb b/notebooks/tutorials/pandas-cookbook/03-which-borough-has-the-most-noise-complaints.ipynb index 747f7c0f792..51443872eb7 100644 --- a/notebooks/tutorials/pandas-cookbook/03-which-borough-has-the-most-noise-complaints.ipynb +++ b/notebooks/tutorials/pandas-cookbook/03-which-borough-has-the-most-noise-complaints.ipynb @@ -874,7 +874,7 @@ }, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "assert isinstance(project, sy.service.project.project.Project)\n", "project" ] diff --git a/notebooks/tutorials/pandas-cookbook/04-weekday-bike-most-groupby-aggregate.ipynb b/notebooks/tutorials/pandas-cookbook/04-weekday-bike-most-groupby-aggregate.ipynb index 278363f5e6d..29878fd826c 100644 --- a/notebooks/tutorials/pandas-cookbook/04-weekday-bike-most-groupby-aggregate.ipynb +++ b/notebooks/tutorials/pandas-cookbook/04-weekday-bike-most-groupby-aggregate.ipynb @@ -634,7 +634,7 @@ }, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "assert isinstance(project, sy.service.project.project.Project)\n", "project" ] diff --git a/notebooks/tutorials/pandas-cookbook/05-combining-dataframes-scraping-weather-data.ipynb b/notebooks/tutorials/pandas-cookbook/05-combining-dataframes-scraping-weather-data.ipynb index 384b8e10701..9afc01da2ec 100644 --- a/notebooks/tutorials/pandas-cookbook/05-combining-dataframes-scraping-weather-data.ipynb +++ b/notebooks/tutorials/pandas-cookbook/05-combining-dataframes-scraping-weather-data.ipynb @@ -821,7 +821,7 @@ }, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "assert isinstance(project, sy.service.project.project.Project)\n", "project" ] diff --git a/notebooks/tutorials/pandas-cookbook/06-string-operations-which-month-was-the-snowiest.ipynb b/notebooks/tutorials/pandas-cookbook/06-string-operations-which-month-was-the-snowiest.ipynb index 404bdc30026..3544f6b82f4 100644 --- a/notebooks/tutorials/pandas-cookbook/06-string-operations-which-month-was-the-snowiest.ipynb +++ b/notebooks/tutorials/pandas-cookbook/06-string-operations-which-month-was-the-snowiest.ipynb @@ -723,7 +723,7 @@ }, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "assert isinstance(project, sy.service.project.project.Project)\n", "project" ] diff --git a/notebooks/tutorials/pandas-cookbook/07-cleaning-up-messy-data.ipynb b/notebooks/tutorials/pandas-cookbook/07-cleaning-up-messy-data.ipynb index c5a1887d04e..f64f8728793 100644 --- a/notebooks/tutorials/pandas-cookbook/07-cleaning-up-messy-data.ipynb +++ b/notebooks/tutorials/pandas-cookbook/07-cleaning-up-messy-data.ipynb @@ -778,7 +778,7 @@ }, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "assert isinstance(project, sy.service.project.project.Project)\n", "project" ] diff --git a/notebooks/tutorials/pandas-cookbook/08-how-to-deal-with-timestamps.ipynb b/notebooks/tutorials/pandas-cookbook/08-how-to-deal-with-timestamps.ipynb index 5bb016f1cae..6d1c11f3153 100644 --- a/notebooks/tutorials/pandas-cookbook/08-how-to-deal-with-timestamps.ipynb +++ b/notebooks/tutorials/pandas-cookbook/08-how-to-deal-with-timestamps.ipynb @@ -728,7 +728,7 @@ }, "outputs": [], "source": [ - "project = new_project.start()\n", + "project = new_project.send()\n", "assert isinstance(project, sy.service.project.project.Project)\n", "project" ] diff --git a/packages/.dockerignore b/packages/.dockerignore deleted file mode 100644 index ba9aa4b6829..00000000000 --- a/packages/.dockerignore +++ /dev/null @@ -1,11 +0,0 @@ -**/*.pyc - -grid/* -!grid/backend - -syftcli - -syft/tests -syft/README.md - -hagrid \ No newline at end of file diff --git a/packages/grid/backend/backend.dockerfile b/packages/grid/backend/backend.dockerfile index dc15ccb1d59..08ea2c9a72a 100644 --- a/packages/grid/backend/backend.dockerfile +++ b/packages/grid/backend/backend.dockerfile @@ -1,94 +1,69 @@ ARG PYTHON_VERSION="3.12" -ARG TZ="Etc/UTC" - -# change to USER="syftuser", UID=1000 and HOME="/home/$USER" for rootless -ARG USER="root" -ARG UID=0 -ARG USER_GRP=$USER:$USER -ARG HOME="/root" -ARG APPDIR="$HOME/app" +ARG UV_VERSION="0.1.41-r0" +ARG TORCH_VERSION="2.3.0" # ==================== [BUILD STEP] Python Dev Base ==================== # -FROM cgr.dev/chainguard/wolfi-base as python_dev +FROM cgr.dev/chainguard/wolfi-base as syft_deps ARG PYTHON_VERSION -ARG TZ -ARG USER -ARG UID +ARG UV_VERSION +ARG TORCH_VERSION # Setup Python DEV -RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \ - apk update && \ - apk upgrade && \ - apk add build-base gcc tzdata python-$PYTHON_VERSION-dev-default py$PYTHON_VERSION-pip && \ - ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# uncomment for creating rootless user -# && adduser -D -u $UID $USER - -# ==================== [BUILD STEP] Install Syft Dependency ==================== # - -FROM python_dev as syft_deps - -ARG APPDIR -ARG HOME -ARG UID -ARG USER -ARG USER_GRP - -USER $USER -WORKDIR $APPDIR -ENV PATH=$PATH:$HOME/.local/bin - -# copy skeleton to do package install -COPY --chown=$USER_GRP \ - syft/setup.py \ - syft/setup.cfg \ - syft/pyproject.toml \ - syft/MANIFEST.in \ - syft/ - -COPY --chown=$USER_GRP \ - syft/src/syft/VERSION \ - syft/src/syft/capnp \ - syft/src/syft/ - -# Install all dependencies together here to avoid any version conflicts across pkgs -RUN --mount=type=cache,id=pip-$UID,target=$HOME/.cache/pip,uid=$UID,gid=$UID,sharing=locked \ - pip install --user --default-timeout=300 torch==2.2.1 -f https://download.pytorch.org/whl/cpu/torch_stable.html && \ - pip install --user pip-autoremove jupyterlab -e ./syft[data_science] && \ - pip-autoremove ansible ansible-core -y +RUN apk update && apk upgrade && \ + apk add build-base gcc python-$PYTHON_VERSION-dev-default uv=$UV_VERSION + +WORKDIR /root/app + +ENV UV_HTTP_TIMEOUT=600 + +# keep static deps separate to have each layer cached independently +# if amd64 then we need to append +cpu to the torch version +# uv issues: https://github.com/astral-sh/uv/issues/3437 & https://github.com/astral-sh/uv/issues/2541 +RUN --mount=type=cache,target=/root/.cache,sharing=locked \ + uv venv && \ + ARCH=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) && \ + if [[ "$ARCH" = "amd64" ]]; then TORCH_VERSION="$TORCH_VERSION+cpu"; fi && \ + uv pip install torch==$TORCH_VERSION --index-url https://download.pytorch.org/whl/cpu + +COPY syft/setup.py syft/setup.cfg syft/pyproject.toml ./syft/ + +COPY syft/src/syft/VERSION ./syft/src/syft/ + +RUN --mount=type=cache,target=/root/.cache,sharing=locked \ + # remove torch because we already have the cpu version pre-installed + sed --in-place /torch==/d ./syft/setup.cfg && \ + uv pip install -e ./syft[data_science] # ==================== [Final] Setup Syft Server ==================== # FROM cgr.dev/chainguard/wolfi-base as backend -# inherit from global -ARG APPDIR -ARG HOME ARG PYTHON_VERSION -ARG TZ -ARG USER -ARG USER_GRP - -# Setup Python -RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \ - apk update && \ - apk upgrade && \ - apk add tzdata git bash python-$PYTHON_VERSION-default py$PYTHON_VERSION-pip && \ - ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \ - # Uncomment for rootless user - # adduser -D -u 1000 $USER && \ - mkdir -p /var/log/pygrid $HOME/data/creds $HOME/data/db $HOME/.cache $HOME/.local -# chown -R $USER_GRP /var/log/pygrid $HOME/ - -USER $USER -WORKDIR $APPDIR +ARG UV_VERSION + +RUN apk update && apk upgrade && \ + apk add --no-cache git bash python-$PYTHON_VERSION-default py$PYTHON_VERSION-pip uv=$UV_VERSION + +WORKDIR /root/app/ + +# Copy pre-built syft dependencies +COPY --from=syft_deps /root/app/.venv .venv + +# copy grid +COPY grid/backend/grid ./grid/ + +# copy syft +COPY syft ./syft/ # Update environment variables -ENV PATH=$PATH:$HOME/.local/bin \ - PYTHONPATH=$APPDIR \ - APPDIR=$APPDIR \ +ENV \ + # "activate" venv + PATH="/root/app/.venv/bin/:$PATH" \ + VIRTUAL_ENV="/root/app/.venv" \ + # Syft + APPDIR="/root/app" \ NODE_NAME="default_node_name" \ NODE_TYPE="domain" \ SERVICE_NAME="backend" \ @@ -102,16 +77,6 @@ ENV PATH=$PATH:$HOME/.local/bin \ MONGO_HOST="localhost" \ MONGO_PORT="27017" \ MONGO_USERNAME="root" \ - MONGO_PASSWORD="example" \ - CREDENTIALS_PATH="$HOME/data/creds/credentials.json" - -# Copy pre-built jupyterlab, syft dependencies -COPY --chown=$USER_GRP --from=syft_deps $HOME/.local $HOME/.local - -# copy grid -COPY --chown=$USER_GRP grid/backend/grid ./grid/ - -# copy syft -COPY --chown=$USER_GRP syft/ ./syft/ + MONGO_PASSWORD="example" CMD ["bash", "./grid/start.sh"] diff --git a/packages/grid/backend/backend.dockerfile.dockerignore b/packages/grid/backend/backend.dockerfile.dockerignore new file mode 100644 index 00000000000..2c06567a214 --- /dev/null +++ b/packages/grid/backend/backend.dockerfile.dockerignore @@ -0,0 +1,63 @@ +# Paths should be against the docker root context dir i.e. /packages + +# Syft +**/tests/ +**/*.md + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] +**/*$py.class + +# Distribution / packaging +**/.Python +**/build/ +**/develop-eggs/ +**/dist/ +**/downloads/ +**/eggs/ +**/.eggs/ +**/lib/ +**/lib64/ +**/parts/ +**/sdist/ +**/var/ +**/wheels/ +**/share/python-wheels/ +**/*.egg-info/ +**/.installed.cfg +**/*.egg +**/MANIFEST + +# Jupyter Notebook +**/.ipynb_checkpoints + +# Environments +**/.env +**/.venv +**/env/ +**/venv/ +**/ENV/ +**/env.bak/ +**/venv.bak/ + +# Unit test / coverage reports +**/htmlcov/ +**/.tox/ +**/.nox/ +**/.coverage +**/.coverage.* +**/.cache +**/nosetests.xml +**/coverage.xml +**/*.cover +**/*.py,cover +**/.hypothesis/ +**/.pytest_cache/ +**/cover/ + +# vim +**/*.swp + +# macOS +**/.DS_Store diff --git a/packages/grid/backend/grid/bootstrap.py b/packages/grid/backend/grid/bootstrap.py index 84fedc36fdf..0da833a3a39 100644 --- a/packages/grid/backend/grid/bootstrap.py +++ b/packages/grid/backend/grid/bootstrap.py @@ -26,8 +26,7 @@ def get_env(key: str, default: str = "") -> str | None: return None -DEFAULT_CREDENTIALS_PATH = os.path.expandvars("$HOME/data/creds/credentials.json") -CREDENTIALS_PATH = str(get_env("CREDENTIALS_PATH", DEFAULT_CREDENTIALS_PATH)) +CREDENTIALS_PATH = str(get_env("CREDENTIALS_PATH", "credentials.json")) NODE_PRIVATE_KEY = "NODE_PRIVATE_KEY" NODE_UID = "NODE_UID" diff --git a/packages/grid/backend/grid/images/worker_cpu.dockerfile b/packages/grid/backend/grid/images/worker_cpu.dockerfile index 717df5817fd..1ecaa950358 100644 --- a/packages/grid/backend/grid/images/worker_cpu.dockerfile +++ b/packages/grid/backend/grid/images/worker_cpu.dockerfile @@ -18,13 +18,12 @@ ARG PIP_PACKAGES="pip --dry-run" ARG CUSTOM_CMD='echo "No custom commands passed"' # Worker specific environment variables go here -ENV SYFT_WORKER="true" -ENV SYFT_VERSION_TAG=${SYFT_VERSION_TAG} +ENV SYFT_WORKER="true" \ + SYFT_VERSION_TAG=${SYFT_VERSION_TAG} \ + UV_HTTP_TIMEOUT=600 -# Commenting this until we support built using python docker sdk or find any other alternative. -# RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \ -# --mount=type=cache,target=$HOME/.cache/pip,sharing=locked \ -RUN apk update && \ - apk add ${SYSTEM_PACKAGES} && \ - pip install --user ${PIP_PACKAGES} && \ +RUN apk update && apk upgrade && \ + apk add --no-cache ${SYSTEM_PACKAGES} && \ + # if uv is present then run uv pip install else simple pip install + if [ -x "$(command -v uv)" ]; then uv pip install --no-cache ${PIP_PACKAGES}; else pip install --user ${PIP_PACKAGES}; fi && \ bash -c "$CUSTOM_CMD" diff --git a/packages/grid/backend/grid/start.sh b/packages/grid/backend/grid/start.sh index 284cf41a268..297f242ff78 100755 --- a/packages/grid/backend/grid/start.sh +++ b/packages/grid/backend/grid/start.sh @@ -1,8 +1,7 @@ #! /usr/bin/env bash set -e -echo "Running start.sh with RELEASE=${RELEASE} and $(id)" -export GEVENT_MONKEYPATCH="False" +echo "Running Syft with RELEASE=${RELEASE} and $(id)" APP_MODULE=grid.main:app LOG_LEVEL=${LOG_LEVEL:-info} @@ -10,15 +9,9 @@ HOST=${HOST:-0.0.0.0} PORT=${PORT:-80} NODE_TYPE=${NODE_TYPE:-domain} APPDIR=${APPDIR:-$HOME/app} - RELOAD="" DEBUG_CMD="" -# For debugging permissions -ls -lisa $HOME/data -ls -lisa $APPDIR/syft/ -ls -lisa $APPDIR/grid/ - if [[ ${DEV_MODE} == "True" ]]; then echo "DEV_MODE Enabled" @@ -28,15 +21,15 @@ fi # only set by kubernetes to avoid conflict with docker tests if [[ ${DEBUGGER_ENABLED} == "True" ]]; then - pip install --user debugpy + uv pip install debugpy DEBUG_CMD="python -m debugpy --listen 0.0.0.0:5678 -m" fi -set +e +export CREDENTIALS_PATH=${CREDENTIALS_PATH:-$HOME/data/creds/credentials.json} export NODE_PRIVATE_KEY=$(python $APPDIR/grid/bootstrap.py --private_key) export NODE_UID=$(python $APPDIR/grid/bootstrap.py --uid) export NODE_TYPE=$NODE_TYPE -set -e +export GEVENT_MONKEYPATCH="False" echo "NODE_UID=$NODE_UID" echo "NODE_TYPE=$NODE_TYPE" diff --git a/packages/grid/frontend/.dockerignore b/packages/grid/frontend/.dockerignore deleted file mode 100644 index 00df28f40b9..00000000000 --- a/packages/grid/frontend/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -.DS_Store -node_modules -.svelte-kit -.pnpm-store \ No newline at end of file diff --git a/packages/grid/frontend/frontend.dockerfile.dockerignore b/packages/grid/frontend/frontend.dockerfile.dockerignore new file mode 100644 index 00000000000..449ac1c92ef --- /dev/null +++ b/packages/grid/frontend/frontend.dockerfile.dockerignore @@ -0,0 +1,15 @@ +# Paths should be relative to the context dir of this image i.e. /packages/grid/frontend/ + +# Frontend +**/*.md + +# Dependency directories +**/node_modules +**/.svelte-kit +**/.pnpm-store + +# vim +**/*.swp + +# macOS +**/.DS_Store diff --git a/packages/grid/scripts/cron.sh b/packages/grid/scripts/cron.sh deleted file mode 100755 index 9b0bfe530a6..00000000000 --- a/packages/grid/scripts/cron.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/bash - -# cronjob logs: $ tail -f /var/log/syslog | grep -i cron - -# $1 is the PySyft dir -# $2 is the git repo like: https://github.com/OpenMined/PySyft -# $3 is the branch like: dev -# $4 is the permission user like: om -# $5 is the permission group like: om -# $6 is the node type like: domain -# $7 is the node name like: node -# $8 is the build directory where we copy the source so we dont trigger hot reloading -# $9 is a bool for enabling tls or not, where true is tls enabled -# $10 is the path to tls certs if available -# $11 release mode, production or development with hot reloading -# $12 docker_tag if set to local, normal local build occurs, otherwise use dockerhub - -# these commands cant be used because they trigger hot reloading -# however without them accidental changes to the working tree might cause issues -# with the fetch process so we should consider changing how this works perhaps by -# copying the code into a folder for execution and keeping the git repo seperate - -# git checkout main --force -# git branch -D $3 || true -# git checkout $3 --force - -pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1 - -cd $1 -START_HASH=$(git rev-parse HEAD) -CURRENT_REMOTE=$(git remote -v | head -n 1 | cut -d ' ' -f 1 | awk '{print $2}') -CURRENT_BRANCH=$(git branch --show-current) -echo "Running autoupdate CRON" - -# does https://github.com/OpenMined/PySyft contain OpenMined/PySyft -if [[ ! "$CURRENT_REMOTE" == *"$2"* ]] -then - echo "Switching remotes to: ${2}" - git remote rm origin || true - git remote add origin https://github.com/$2 - git fetch origin - echo "Checking out branch: ${3}" - git reset --hard "origin/${3}" - git checkout "origin/${3}" --force - chown -R $4:$5 . -fi - -if [ "$CURRENT_BRANCH" != "$3" ] -then - echo "Checking out branch: ${3}" -fi - -git fetch origin -git reset --hard "origin/${3}" -git checkout "origin/${3}" --force -chown -R $4:$5 . - -END_HASH=$(git rev-parse HEAD) -CONTAINER_VERSION=$(docker ps --format "{{.Names}}" | grep 'backend' | head -1l | xargs -I {} docker exec {} env | grep ^VERSION= | sed 's/VERSION=//') -CONTAINER_HASH=$(docker ps --format "{{.Names}}" | grep 'backend' | head -1l | xargs -I {} docker exec {} env | grep VERSION_HASH | sed 's/VERSION_HASH=//') - -REDEPLOY="0" - - -# see hagrid --release options -if [[ ${11} = "development" ]]; then - RELEASE=development -else - RELEASE=production -fi - -if [[ -z "${12}" ]]; then - DOCKER_TAG="local" -else - DOCKER_TAG="${12}" -fi - -if [[ "$CONTAINER_HASH" == "dockerhub" ]] -then - echo "Version: $CONTAINER_VERSION from dockerhub deployed" -elif [[ "$START_HASH" != "$END_HASH" ]] -then - echo "Git hashes $START_HASH vs $END_HASH dont match, redeploying" - REDEPLOY="1" -elif [[ ! "$END_HASH" == *"$CONTAINER_HASH"* ]] -then - echo "Container hash $END_HASH not in $CONTAINER_HASH, redeploying" - REDEPLOY="1" -elif [[ -z "$CONTAINER_HASH" ]] -then - echo "Container hash $CONTAINER_HASH is not valid, redeploying" - REDEPLOY="1" -fi - -echo "START_HASH=$START_HASH" -echo "END_HASH=$END_HASH" -echo "CONTAINER_HASH=$CONTAINER_HASH" -echo "REDEPLOY=$REDEPLOY" - -if [[ ${REDEPLOY} != "0" ]]; then - bash /home/om/PySyft/packages/grid/scripts/redeploy.sh $1 $2 $3 $4 $5 $6 $7 $8 $9 ${10} ${RELEASE} ${DOCKER_TAG} -fi - -echo "Finished autoupdate CRON" diff --git a/packages/grid/scripts/redeploy.sh b/packages/grid/scripts/redeploy.sh deleted file mode 100755 index 121f11c62f3..00000000000 --- a/packages/grid/scripts/redeploy.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# only run one redeploy.sh at a time -pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1 - -# cronjob logs: $ tail -f /var/log/syslog | grep -i cron - -# $1 is the PySyft dir -# $2 is the git repo like: https://github.com/OpenMined/PySyft -# $3 is the branch like: dev -# $4 is the permission user like: om -# $5 is the permission group like: om -# $6 is the node type like: domain -# $7 is the node name like: node -# $8 is the build directory where we copy the source so we dont trigger hot reloading -# $9 is a bool for enabling tls or not, where true is tls enabled -# $10 is the path to tls certs if available -# $11 release mode, production or development with hot reloading -# $12 docker_tag if set to local, normal local build occurs, otherwise use dockerhub - -if [[ ${11} = "development" ]]; then - RELEASE=development -else - RELEASE=production -fi - -if [[ -z "${12}" ]]; then - DOCKER_TAG="local" -else - DOCKER_TAG="${12}" -fi - -echo "Code has changed so redeploying with HAGrid" -rm -rf ${8} -cp -r ${1} ${8} -chown -R ${4}:${5} ${8} -/usr/sbin/runuser -l ${4} -c "pip install -e ${8}/packages/hagrid" -# /usr/sbin/runuser -l ${4} -c "hagrid launch ${7} ${6} to localhost --repo=${2} --branch=${3} --ansible-extras='docker_volume_destroy=true'" -if [[ "${9}" = "true" ]]; then - echo "Starting Grid with TLS" - HAGRID_CMD="hagrid launch ${7} ${6} to localhost --repo=${2} --branch=${3} --release=${RELEASE} --tag=${DOCKER_TAG} --tls --cert-store-path=${10}" - echo $HAGRID_CMD - /usr/sbin/runuser -l ${4} -c "$HAGRID_CMD" -else - echo "Starting Grid without TLS" - HAGRID_CMD="hagrid launch ${7} ${6} to localhost --repo=${2} --branch=${3} --release=${RELEASE} --tag=${DOCKER_TAG}" - echo $HAGRID_CMD - /usr/sbin/runuser -l ${4} -c "$HAGRID_CMD" -fi diff --git a/packages/grid/seaweedfs/seaweedfs.dockerfile.dockerignore b/packages/grid/seaweedfs/seaweedfs.dockerfile.dockerignore new file mode 100644 index 00000000000..98a48c5b17d --- /dev/null +++ b/packages/grid/seaweedfs/seaweedfs.dockerfile.dockerignore @@ -0,0 +1,63 @@ +# Paths should be relative to the context dir of this image i.e. /packages/grid/seaweedfs/ + +# SeaweedFS +**/tests/ +**/*.md + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] +**/*$py.class + +# Distribution / packaging +**/.Python +**/build/ +**/develop-eggs/ +**/dist/ +**/downloads/ +**/eggs/ +**/.eggs/ +**/lib/ +**/lib64/ +**/parts/ +**/sdist/ +**/var/ +**/wheels/ +**/share/python-wheels/ +**/*.egg-info/ +**/.installed.cfg +**/*.egg +**/MANIFEST + +# Jupyter Notebook +**/.ipynb_checkpoints + +# Environments +**/.env +**/.venv +**/env/ +**/venv/ +**/ENV/ +**/env.bak/ +**/venv.bak/ + +# Unit test / coverage reports +**/htmlcov/ +**/.tox/ +**/.nox/ +**/.coverage +**/.coverage.* +**/.cache +**/nosetests.xml +**/coverage.xml +**/*.cover +**/*.py,cover +**/.hypothesis/ +**/.pytest_cache/ +**/cover/ + +# vim +**/*.swp + +# macOS +**/.DS_Store diff --git a/packages/grid/syft-client/syft.Dockerfile b/packages/grid/syft-client/syft.Dockerfile index e3d1189a8e8..8f94e38b81b 100644 --- a/packages/grid/syft-client/syft.Dockerfile +++ b/packages/grid/syft-client/syft.Dockerfile @@ -14,8 +14,7 @@ RUN apk update && apk upgrade && \ COPY ./syft /tmp/syft RUN --mount=type=cache,target=/root/.cache,sharing=locked \ - pip install --user jupyterlab==4.1.6 pip-autoremove==0.10.0 /tmp/syft && \ - pip-autoremove ansible ansible-core -y + pip install --user jupyterlab==4.1.6 /tmp/syft # ==================== [Final] Setup Syft Client ==================== # diff --git a/packages/grid/syft-client/syft.Dockerfile.dockerignore b/packages/grid/syft-client/syft.Dockerfile.dockerignore index c5bacaa51c3..d78459cecbb 100644 --- a/packages/grid/syft-client/syft.Dockerfile.dockerignore +++ b/packages/grid/syft-client/syft.Dockerfile.dockerignore @@ -1,67 +1,67 @@ # Syft -tests/ -*.md +**/tests/ +**/*.md # Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +**/__pycache__/ +**/*.py[cod] +**/*$py.class # Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST +**/.Python +**/build/ +**/develop-eggs/ +**/dist/ +**/downloads/ +**/eggs/ +**/.eggs/ +**/lib/ +**/lib64/ +**/parts/ +**/sdist/ +**/var/ +**/wheels/ +**/share/python-wheels/ +**/*.egg-info/ +**/.installed.cfg +**/*.egg +**/MANIFEST # Jupyter Notebook -.ipynb_checkpoints +**/.ipynb_checkpoints # IPython -profile_default/ -ipython_config.py +**/profile_default/ +**/ipython_config.py # Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ +**/.env +**/.venv +**/env/ +**/venv/ +**/ENV/ +**/env.bak/ +**/venv.bak/ # Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ +**/htmlcov/ +**/.tox/ +**/.nox/ +**/.coverage +**/.coverage.* +**/.cache +**/nosetests.xml +**/coverage.xml +**/*.cover +**/*.py,cover +**/.hypothesis/ +**/.pytest_cache/ +**/cover/ # mypy -.mypy_cache/ -.dmypy.json -dmypy.json +**/.mypy_cache/ +**/.dmypy.json +**/dmypy.json # macOS -.DS_Store +**/.DS_Store diff --git a/packages/hagrid/.bumpversion.cfg b/packages/hagrid/.bumpversion.cfg deleted file mode 100644 index a30678ab82f..00000000000 --- a/packages/hagrid/.bumpversion.cfg +++ /dev/null @@ -1,12 +0,0 @@ -[bumpversion] -current_version = 0.3.121 -tag = False -tag_name = {new_version} -commit = True -commit_message = Bump version: {current_version} β†’ {new_version} - -[bumpversion:file:hagrid/version.py] - -[bumpversion:file:setup.py] - -[bumpversion:file:hagrid/manifest_template.yml] diff --git a/packages/hagrid/.dockerignore b/packages/hagrid/.dockerignore deleted file mode 100644 index 1effc400690..00000000000 --- a/packages/hagrid/.dockerignore +++ /dev/null @@ -1,11 +0,0 @@ -.git -.vscode -.mypy_cache -.benchmarks -build -dist -hagrid.egg-info -Pipfile -Dockerfile -README.md -.venv diff --git a/packages/hagrid/.gitattributes b/packages/hagrid/.gitattributes deleted file mode 100644 index dfe0770424b..00000000000 --- a/packages/hagrid/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/packages/hagrid/.gitignore b/packages/hagrid/.gitignore deleted file mode 100644 index 7ebe0d7df82..00000000000 --- a/packages/hagrid/.gitignore +++ /dev/null @@ -1,128 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -Pipfile -.envfile diff --git a/packages/hagrid/README.md b/packages/hagrid/README.md deleted file mode 100644 index a2d398960c1..00000000000 --- a/packages/hagrid/README.md +++ /dev/null @@ -1,219 +0,0 @@ -# hagrid - -Use this cli to deploy PyGrid Domain and Network nodes on your local machine. - -A Hagrid is a HAppy GRID! - -## Installation Linux and MacOS - -Python - -``` -$ pip install -U hagrid -``` - -Docker - -``` -$ docker run -it -v ~/:/root openmined/hagrid:latest hagrid -``` - -Then simply run hagrid as you would normally: - -``` -$ docker run -it -v ~/:/root openmined/hagrid:latest hagrid launch slytherin domain to azure -``` - -## Installation Windows - -Requirements: - -- docker - -### Docker - -You can manually install Docker Desktop: https://www.docker.com/products/docker-desktop - -or alternatively use the windows package manager `chocolatey`. - -### Chocolatey - -To install `chocolatey` you need to open "Windows PowerShell" in Administrator mode. -You can type `powershell` into the search bar and right-click and select "Run as administrator". If when you start Powershell in Administrator mode it asks you whether you want to allow powershell to make changes to your computer, select "Yes". -Then copy and paste this command and run it. - -Run this inside PowerShell (Admin Mode): - -```powershell -Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) -``` - -You should now be able to type: `choco` in PowerShell terminal to use the tool. - -### Docker with Chocolatey - -To install Docker Desktop with chocolatey run this inside PowerShell (Admin Mode): - -```powershell -choco install docker-desktop -y -``` - -You will likely need to restart after installing Docker Desktop for the first time. Also occasionally Powershell looks like its taking for ever but if you hit then it'll show that the command is actually done. - -### Restart the computer and start Docker Desktop - -After you've rebooted your Windows machine, launch the application "Docker Desktop" and continue. - -### Docker Backend - -Docker on Windows has two possible backends, one which uses a virtual machine and the other which uses Windows Subsystem for Linux 2. - -Try running Docker Desktop and seeing that it starts. -If you get an error saying "Docker Engine failed to start..." you may not have Virtualization enabled. -Either have it enabled in your BIOS if your CPU supports it, or you may need to use the Windows Subsystem for Linux 2 backend. - -To install WSL2 with chocolately run this inside PowerShell (Admin Mode): - -```powershell -choco install wsl2 -y -``` - -If you needed to install wsl2, restart docker by clicking the little whale in the bottom right corner, clicking "Stop" and then starting Docker Desktop application again. - -### Enable Docker Compose v2 - -Inside Docker Desktop click on the settings wheel in the top right. -Click on the menu item "Experimental Features" on the left. -Check the box that says: "Use Docker Desktop V2". - -## SSH Keys - -HAGrid allows you to select an SSH key, to setup a remote node. When using Docker on Windows we recommend you mount your Users %USERPROFILE% directory into the container so that any keys you already have can be accessed. If you have a key inside C:\Users\John Smith\.ssh\mykey.pem then when asked for the path to your key you would enter: `~/mykey.pem`. - -If HAGrid complains that you have no key, it can generate one for you, or you can always generate one yourself manually using the ssh-keygen.exe tool. - -To generate a key using ssh-keygen run in a Powershell: - -``` -ssh-keygen -``` - -Unless you know what the options are simply pressing enter and going with the defaults is perfectly fine. This will create a file called `~/.ssh/id_rsa` which is also the default that HAGrid asks you if you want to use. - -## Run HAGrid Docker Container - -```powershell -docker run -it -v "$($env:USERPROFILE):/root" openmined/hagrid:latest hagrid -``` - -Then simply run hagrid as you would normally: - -```powershell -docker run -it -v "$($env:USERPROFILE):/root" openmined/hagrid:latest hagrid launch slytherin to azure -``` - -## Development - -#### Step 1 Dev Setup - -If you want hagrid to launch nodes based on your live-updating codebase, then install it using one of the live-updating install commands. This will mean that the codebase will hot-reload based on the current project. - -```bash -pip install -e . -``` - -## Launch a Node - -![alt text](cli2.png) - -## A Few Example Commands - -Start a node with: - -```bash -hagrid launch slytherin -``` - -... and then stop it with: - -```bash -hagrid land slytherin -``` - -You can specify ports if you want to: - -```bash -hagrid launch hufflepuff_house to docker:8081+ -``` - -... but if you don't it'll find an open one for you - -```bash -// finds hufflepuff already has 8081... tries 8082 -hagrid launch ravenclaw -``` - -You can also specify the node type (domain by default) - -```bash -hagrid launch gryffendor network to docker -``` - -## Credits - -## Testing HAGrid Remotely - -Sometimes you need to install HAGrid directly from source while developing and testing on a remote machine. You can use the pip git syntax like so: - -``` -$ pip install "git+https://github.com/OpenMined/PySyft@demo_strike_team_branch_4#subdirectory=packages/hagrid" -``` - -## Deploying HAGrid to a running Linux Machine (Ubuntu 20.x) - -Log into your linux machine and run the following: - -``` -pip install -U hagrid -``` - -Often on a remote linux box hagrid will not by default show up in the path. Re-login via SSH terminal and hagrid should appear. - -``` -hagrid launch domain to localhost -``` - -Then folllow the instructions in the prompt. Note that occasionally you'll see a harmless (but red and scary looking) error at the very end of the deploy which looks something like: - -``` -"Container slytherin_flower_1 Starting", "Container slytherin_db_1 Starting", "Container slytherin_proxy_1 Starting", "Container slytherin_queue_1 Starting", "Container slytherin_queue_1 Started", "Container slytherin_db_1 Started", "Container slytherin_backend_1 Starting", "Container slytherin_celeryworker_1 Starting", "Container slytherin_pgadmin_1 Starting", "Container slytherin_proxy_1 Started", "Container slytherin_flower_1 Started", "Container slytherin_backend_1 Started", "Container slytherin_celeryworker_1 Started", "Container slytherin_pgadmin_1 Started", "Container slytherin_backend_stream_1 Starting", "Container slytherin_frontend_1 Starting", "Container slytherin_frontend_1 Started", "Container slytherin_backend_stream_1 Started", "Error response from daemon: cannot start a stopped process: unknown"], "stdout": "", "stdout_lines": []} -: cannot start a stopped process: unknown"], "stdout": "", "stdout_lines": []} - -PLAY RECAP *************************************************************************************************************************** -104.42.1.158 : ok=26 changed=21 unreachable=0 failed=1 skipped=1 rescued=0 ignored=0 -``` - -If you see this don't worry about it. - -### Post install checks. - -Log into the openmined user. - -``` -sudo su - om -``` - -Check that the autoupdater is running correctly and pointed to the branch you specified. - -``` -sudo crontab -l -``` - -Should return something like - -``` -#Ansible: Update PySyft Repo -* * * * * /home/om/PySyft/packages/grid/scripts/cron.sh /home/om/PySyft The-PET-Lab-at-the-UN-PPTTT/PySyft ungp_pet_lab om om domain slytherin -``` - -**Super Cool Code Images** by [Carbon](https://carbon.now.sh/) diff --git a/packages/hagrid/build_docker.ps1 b/packages/hagrid/build_docker.ps1 deleted file mode 100644 index 29951ee1e4b..00000000000 --- a/packages/hagrid/build_docker.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -$env:HAGRID_VERSION = $(python hagrid/version.py) -docker buildx build -f hagrid.dockerfile -t openmined/hagrid:"$env:HAGRID_VERSION" -t openmined/hagrid:latest . \ No newline at end of file diff --git a/packages/hagrid/build_docker.sh b/packages/hagrid/build_docker.sh deleted file mode 100755 index 8c0bf150398..00000000000 --- a/packages/hagrid/build_docker.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -HAGRID_VERSION=$(python3 hagrid/version.py) -docker buildx build -f hagrid.dockerfile -t openmined/hagrid:"${HAGRID_VERSION}" -t openmined/hagrid:latest . diff --git a/packages/hagrid/build_wheel.sh b/packages/hagrid/build_wheel.sh deleted file mode 100755 index a5343a06a69..00000000000 --- a/packages/hagrid/build_wheel.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -rm -rf build dist -python setup.py bdist_wheel -rm -rf build diff --git a/packages/hagrid/cli2.png b/packages/hagrid/cli2.png deleted file mode 100644 index 1a31e027921..00000000000 Binary files a/packages/hagrid/cli2.png and /dev/null differ diff --git a/packages/hagrid/hagrid.dockerfile b/packages/hagrid/hagrid.dockerfile deleted file mode 100644 index 878aff613df..00000000000 --- a/packages/hagrid/hagrid.dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM python:3.12-slim as build - -WORKDIR /hagrid -COPY ./ /hagrid - -RUN pip install --upgrade pip setuptools wheel twine -RUN python setup.py bdist_wheel -RUN twine check `find -L ./dist -name "*.whl"` - -FROM python:3.12-slim as backend - -# set UTC timezone -ENV TZ=Etc/UTC -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN DEBIAN_FRONTEND=noninteractive \ - apt-get update && \ - apt-get install -yqq \ - git && \ - rm -rf /var/lib/apt/lists/* - -COPY --from=build /hagrid/dist /hagrid -RUN pip install `find -L /hagrid -name "*.whl"` - -# warm the cache -RUN hagrid -CMD hagrid diff --git a/packages/hagrid/hagrid/__init__.py b/packages/hagrid/hagrid/__init__.py deleted file mode 100644 index eabd22f9f19..00000000000 --- a/packages/hagrid/hagrid/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -from .git_check import verify_git_installation # noqa - -# stdlib -import sys -from typing import Any - -# relative -from .cli import check_status as check # noqa: F401 -from .quickstart_ui import QuickstartUI -from .version import __version__ # noqa: F401 -from .wizard_ui import WizardUI - -from .orchestra import Orchestra # noqa - - -def module_property(func: Any) -> None: - """Decorator to turn module functions into properties. - Function names must be prefixed with an underscore.""" - module = sys.modules[func.__module__] - - def base_getattr(name: str) -> None: - raise AttributeError(f"module '{module.__name__}' has no attribute '{name}'") - - old_getattr = getattr(module, "__getattr__", base_getattr) - - def new_getattr(name: str) -> Any: - if f"_{name}" == func.__name__: - return func() - else: - return old_getattr(name) - - module.__getattr__ = new_getattr # type: ignore - return func - - -@module_property -def _quickstart() -> QuickstartUI: - return QuickstartUI() - - -@module_property -def _wizard() -> WizardUI: - return WizardUI() diff --git a/packages/hagrid/hagrid/art.py b/packages/hagrid/hagrid/art.py deleted file mode 100644 index a272ab9d52f..00000000000 --- a/packages/hagrid/hagrid/art.py +++ /dev/null @@ -1,125 +0,0 @@ -# stdlib -import locale -import secrets - -# third party -import ascii_magic -import rich -from rich.emoji import Emoji - - -def motorcycle() -> None: - print( - """ - ` - `.+yys/.` - ``/NMMMNNs` - `./shNMMMMMMNs`` `..` - `-smNMMNNMMMMMMN/.``......` - `.yNMMMMNmmmmNNMMm/.`....` - `:sdNMMMMMMNNNNddddds-`.`` `--. ` - `.+dNNNNMMMMMMMMMNNNNmddohmh//hddy/.```..` - `-hNMMMMMMMMMMMMNNdmNNMNNdNNd:sdyoo+/++:..` - ../mMMMMMMMMMMMMMMNNmmmmNMNmNNmdmd/hNNNd+:` - `:mMNNMMMMMMMMMMMMNMNNmmmNNNNNdNNd/NMMMMm:: - `:mMNNNMMMMMMMMMMMMMMMNNNNdNMNNmmNd:smMMmh// - ``/mMMMMMMMMMMMMMMMMMMMMMMNmdmNNMMNNNy/osoo/-` - `-sNMMMMMMMMMMMMMMMMMMMMMMMMNNmmMMMMNh-....` - `/dNMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMNy.` - ``.omNNMMMMMMMMMMMMNMMMMMMMNmmmmNNMMMMN+` - `:hmNNMMMMMMMMMMMNo/ohNNNNho+os+-+hNys/` - -mNNNNNNMMMMMMMMm+``-yNdd+/mMMMms.-:` - .+dmNNNNMMMMMMNd:``:dNN+y`oMMMMMm-.` - `+dmmmNNNmmmmy+. `-+m/s/+MMMMm/-- - `+mmmhNy/-...``` ``-.-sosyys+/-` - ``.smmmsoo`` .oh+-:/:. - `.:odmdh/```` `.+d+`````` - ```/sydNdhy+.` ``-sds. - `:hdmhs::-```` `oNs.` -```.sdmh/`` `-ym+` - ``ssy+` `-yms.` - `` `:hNy-`` - ` `-yMN/``` - `-yNhy- - `/yNd/` - `:dNMs`` - `.+mNy/.` - `.+hNMMs`` - `:dMMMMh.`""" # noqa: W605 - ) - - -def hold_on_tight() -> None: - pass - - -def hagrid1() -> None: - # relative - from .lib import asset_path - - try: - ascii_magic.to_terminal( - ascii_magic.from_image_file( - img_path=str(asset_path()) + "/img/hagrid.png", columns=83 - ) - ) - except Exception: # nosec - pass - - -def hagrid2() -> None: - # relative - from .lib import asset_path - - try: - ascii_magic.to_terminal( - ascii_magic.from_image_file( - img_path=str(asset_path()) + "/img/hagrid2.png", columns=83 - ) - ) - except Exception: # nosec - pass - - -def quickstart_art() -> None: - text = """ -888 888 d8888 .d8888b. d8b 888 -888 888 d88888 d88P Y88b Y8P 888 -888 888 d88P888 888 888 888 -8888888888 d88P 888 888 888d888 888 .d88888 -888 888 d88P 888 888 88888 888P" 888 d88" 888 -888 888 d88P 888 888 888 888 888 888 888 -888 888 d8888888888 Y88b d88P 888 888 Y88b 888 -888 888 d88P 888 "Y8888P88 888 888 "Y88888 - - - .d88888b. d8b 888 888 888 -d88P" "Y88b Y8P 888 888 888 -888 888 888 888 888 -888 888 888 888 888 .d8888b 888 888 .d8888b 888888 8888b. 888d888 888888 -888 888 888 888 888 d88P" 888 .88P 88K 888 "88b 888P" 888 -888 Y8b 888 888 888 888 888 888888K "Y8888b. 888 .d888888 888 888 -Y88b.Y8b88P Y88b 888 888 Y88b. 888 "88b X88 Y88b. 888 888 888 Y88b. - "Y888888" "Y88888 888 "Y8888P 888 888 88888P' "Y888 "Y888888 888 "Y888 - Y8b -""" - console = rich.get_console() - console.print( - text, - style="bold", - justify="left", - new_line_start=True, - ) - - -def hagrid() -> None: - """Print a random hagrid image with the caption "hold on tight harry".""" - options = [motorcycle, hagrid1, hagrid2] - i = secrets.randbelow(3) - options[i]() - hold_on_tight() - - -class RichEmoji(Emoji): - def to_str(self) -> str: - return self._char.encode("utf-8").decode(locale.getpreferredencoding()) diff --git a/packages/hagrid/hagrid/auth.py b/packages/hagrid/hagrid/auth.py deleted file mode 100644 index b3cca8a35e5..00000000000 --- a/packages/hagrid/hagrid/auth.py +++ /dev/null @@ -1,25 +0,0 @@ -# stdlib - - -class AuthCredentials: - def __init__( - self, - username: str, - key_path: str | None = None, - password: str | None = None, - ) -> None: - self.username = username - self.key_path = key_path - self.password = password - - @property - def uses_key(self) -> bool: - return bool(self.username and self.key_path) - - @property - def uses_password(self) -> bool: - return bool(self.username and self.password) - - @property - def valid(self) -> bool: - return bool(self.uses_key or self.uses_password) diff --git a/packages/hagrid/hagrid/azure.py b/packages/hagrid/hagrid/azure.py deleted file mode 100644 index b84e1f32bd7..00000000000 --- a/packages/hagrid/hagrid/azure.py +++ /dev/null @@ -1,67 +0,0 @@ -# stdlib -import json -import os -import subprocess # nosec - -# third party -from azure.identity import ClientSecretCredential -from azure.mgmt.resource import ResourceManagementClient - -# relative -from .file import user_hagrid_profile - -AZURE_SERVICE_PRINCIPAL_PATH = f"{user_hagrid_profile}/azure_sp.json" - - -class AzureException(Exception): - pass - - -def check_azure_authed() -> bool: - try: - azure_service_principal() - return True - except AzureException as e: - print(e) - - return False - - -def login_azure() -> bool: - cmd = "az login" - try: - subprocess.check_call(cmd, shell=True, stdout=subprocess.DEVNULL) # nosec - return True - except Exception: # nosec - pass - return False - - -def azure_service_principal() -> dict[str, str] | None: - sp_json = {} - if not os.path.exists(AZURE_SERVICE_PRINCIPAL_PATH): - raise AzureException("No service principal so we need to create one first") - with open(AZURE_SERVICE_PRINCIPAL_PATH) as f: - sp_json = json.loads(f.read()) - - required_keys = ["appId", "displayName", "name", "password", "tenant"] - for key in required_keys: - if key not in sp_json: - raise AzureException(f"{key} missing from {AZURE_SERVICE_PRINCIPAL_PATH}") - return sp_json - - -def azure_credentials( - tenant_id: str, client_id: str, client_secret: str -) -> ClientSecretCredential: - return ClientSecretCredential( - tenant_id=tenant_id, - client_id=client_id, - client_secret=client_secret, - ) - - -def resource_management_client( - credentials: ClientSecretCredential, subscription_id: str -) -> ResourceManagementClient: - return ResourceManagementClient(credentials, subscription_id) diff --git a/packages/hagrid/hagrid/cache.py b/packages/hagrid/hagrid/cache.py deleted file mode 100644 index 5a94d6180aa..00000000000 --- a/packages/hagrid/hagrid/cache.py +++ /dev/null @@ -1,69 +0,0 @@ -# stdlib -import json -import os -from typing import Any - -STABLE_BRANCH = "0.8.6" -DEFAULT_BRANCH = "0.8.6" -DEFAULT_REPO = "OpenMined/PySyft" - -arg_defaults = { - "repo": DEFAULT_REPO, - "branch": STABLE_BRANCH, - "username": "root", - "auth_type": "key", - "key_path": "~/.ssh/id_rsa", - "azure_repo": DEFAULT_REPO, - "azure_branch": STABLE_BRANCH, - "azure_username": "azureuser", - "azure_key_path": "~/.ssh/id_rsa", - "azure_resource_group": "openmined", - "azure_location": "westus", - "azure_size": "Standard_D4s_v3", - "gcp_zone": "us-central1-c", - "gcp_machine_type": "e2-standard-4", - "gcp_project_id": "", - "gcp_username": "", - "gcp_key_path": "~/.ssh/google_compute_engine", - "gcp_repo": DEFAULT_REPO, - "gcp_branch": STABLE_BRANCH, - "install_wizard_complete": False, - "aws_region": "us-east-1", - "aws_security_group_name": "openmined_sg", - "aws_security_group_cidr": "0.0.0.0/0", - "aws_image_id": "ami-05de688637f3e33ee", # Ubuntu Server 22.04 LTS (HVM), SSD Volume Type - "aws_ec2_instance_type": "t2.xlarge", - "aws_ec2_instance_username": "ubuntu", # For Ubuntu AMI, the default user name is ubuntu - "aws_repo": DEFAULT_REPO, - "aws_branch": STABLE_BRANCH, -} - - -class ArgCache: - @staticmethod - def cache_file_path() -> str: - dir_path = os.path.expanduser("~/.hagrid") - os.makedirs(dir_path, exist_ok=True) - return f"{dir_path}/cache.json" - - def __init__(self) -> None: - try: - with open(ArgCache.cache_file_path()) as f: - self.__cache = json.loads(f.read()) - except Exception: # nosec - self.__cache = {} - - def __setitem__(self, key: str, value: Any) -> None: - self.__cache[key] = value - with open(ArgCache.cache_file_path(), "w") as f: - f.write(json.dumps(self.__cache)) - - def __getitem__(self, key: str) -> Any: - if key in self.__cache: - return self.__cache[key] - elif key in arg_defaults: - return arg_defaults[key] - raise KeyError(f"Can't find key {key} in ArgCache") - - -arg_cache = ArgCache() diff --git a/packages/hagrid/hagrid/cli.py b/packages/hagrid/hagrid/cli.py deleted file mode 100644 index 0e8efdc06a4..00000000000 --- a/packages/hagrid/hagrid/cli.py +++ /dev/null @@ -1,4404 +0,0 @@ -# stdlib -from collections import namedtuple -from collections.abc import Callable -from enum import Enum -import json -import os -from pathlib import Path -import platform -from queue import Queue -import re -import shutil -import socket -import stat -import subprocess # nosec -import sys -import tempfile -from threading import Event -from threading import Thread -import time -from typing import Any -from typing import cast -from urllib.parse import urlparse -import webbrowser - -# third party -import click -import requests -import rich -from rich.console import Console -from rich.live import Live -from rich.progress import BarColumn -from rich.progress import Progress -from rich.progress import SpinnerColumn -from rich.progress import TextColumn -from virtualenvapi.manage import VirtualEnvironment - -# relative -from .art import RichEmoji -from .art import hagrid -from .art import quickstart_art -from .auth import AuthCredentials -from .cache import DEFAULT_BRANCH -from .cache import DEFAULT_REPO -from .cache import arg_cache -from .deps import DEPENDENCIES -from .deps import LATEST_BETA_SYFT -from .deps import allowed_hosts -from .deps import check_docker_service_status -from .deps import check_docker_version -from .deps import check_grid_docker -from .deps import gather_debug -from .deps import get_version_string -from .deps import is_windows -from .exceptions import MissingDependency -from .grammar import BadGrammar -from .grammar import GrammarVerb -from .grammar import parse_grammar -from .land import get_land_verb -from .launch import get_launch_verb -from .lib import GIT_REPO -from .lib import GRID_SRC_PATH -from .lib import GRID_SRC_VERSION -from .lib import check_api_metadata -from .lib import check_host -from .lib import check_jupyter_server -from .lib import check_login_page -from .lib import commit_hash -from .lib import docker_desktop_memory -from .lib import find_available_port -from .lib import generate_process_status_table -from .lib import generate_user_table -from .lib import gitpod_url -from .lib import hagrid_root -from .lib import is_gitpod -from .lib import name_tag -from .lib import save_vm_details_as_json -from .lib import update_repo -from .lib import use_branch -from .mode import EDITABLE_MODE -from .parse_template import deployment_dir -from .parse_template import get_template_yml -from .parse_template import manifest_cache_path -from .parse_template import render_templates -from .parse_template import setup_from_manifest_template -from .quickstart_ui import fetch_notebooks_for_url -from .quickstart_ui import fetch_notebooks_from_zipfile -from .quickstart_ui import quickstart_download_notebook -from .rand_sec import generate_sec_random_password -from .stable_version import LATEST_STABLE_SYFT -from .style import RichGroup -from .util import fix_windows_virtualenv_api -from .util import from_url -from .util import shell - -# fix VirtualEnvironment bug in windows -fix_windows_virtualenv_api(VirtualEnvironment) - - -class NodeSideType(Enum): - LOW_SIDE = "low" - HIGH_SIDE = "high" - - -def get_azure_image(short_name: str) -> str: - prebuild_070 = ( - "madhavajay1632269232059:openmined_mj_grid_domain_ubuntu_1:domain_070:latest" - ) - fresh_ubuntu = "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest" - if short_name == "default": - return fresh_ubuntu - elif short_name == "domain_0.7.0": - return prebuild_070 - raise Exception(f"Image name doesn't exist: {short_name}. Try: default or 0.7.0") - - -@click.group(cls=RichGroup) -def cli() -> None: - pass - - -def get_compose_src_path( - node_name: str, - template_location: str | None = None, - **kwargs: Any, -) -> str: - grid_path = GRID_SRC_PATH() - tag = kwargs["tag"] - # Use local compose files if in editable mode and - # template_location is None and (kwargs["dev"] is True or tag is local) - if ( - EDITABLE_MODE - and template_location is None - and (kwargs["dev"] is True or tag == "local") - ): - path = grid_path - else: - path = deployment_dir(node_name) - - os.makedirs(path, exist_ok=True) - return path - - -@click.command( - help="Restore some part of the hagrid installation or deployment to its initial/starting state.", - context_settings={"show_default": True}, -) -@click.argument("location", type=str, nargs=1) -def clean(location: str) -> None: - if location == "library" or location == "volumes": - print("Deleting all Docker volumes in 2 secs (Ctrl-C to stop)") - time.sleep(2) - subprocess.call("docker volume rm $(docker volume ls -q)", shell=True) # nosec - - if location == "containers" or location == "pantry": - print("Deleting all Docker containers in 2 secs (Ctrl-C to stop)") - time.sleep(2) - subprocess.call("docker rm -f $(docker ps -a -q)", shell=True) # nosec - - if location == "images": - print("Deleting all Docker images in 2 secs (Ctrl-C to stop)") - time.sleep(2) - subprocess.call("docker rmi $(docker images -q)", shell=True) # nosec - - -@click.command( - help="Start a new PyGrid domain/network node!", - context_settings={"show_default": True}, -) -@click.argument("args", type=str, nargs=-1) -@click.option( - "--username", - default=None, - required=False, - type=str, - help="Username for provisioning the remote host", -) -@click.option( - "--key-path", - default=None, - required=False, - type=str, - help="Path to the key file for provisioning the remote host", -) -@click.option( - "--password", - default=None, - required=False, - type=str, - help="Password for provisioning the remote host", -) -@click.option( - "--repo", - default=None, - required=False, - type=str, - help="Repo to fetch source from", -) -@click.option( - "--branch", - default=None, - required=False, - type=str, - help="Branch to monitor for updates", -) -@click.option( - "--tail", - is_flag=True, - help="Tail logs on launch", -) -@click.option( - "--headless", - is_flag=True, - help="Start the frontend container", -) -@click.option( - "--cmd", - is_flag=True, - help="Print the cmd without running it", -) -@click.option( - "--jupyter", - is_flag=True, - help="Enable Jupyter Notebooks", -) -@click.option( - "--in-mem-workers", - is_flag=True, - help="Enable InMemory Workers", -) -@click.option( - "--enable-signup", - is_flag=True, - help="Enable Signup for Node", -) -@click.option( - "--build", - is_flag=True, - help="Disable forcing re-build", -) -@click.option( - "--no-provision", - is_flag=True, - help="Disable provisioning VMs", -) -@click.option( - "--node-count", - default=1, - required=False, - type=click.IntRange(1, 250), - help="Number of independent nodes/VMs to launch", -) -@click.option( - "--auth-type", - default=None, - type=click.Choice(["key", "password"], case_sensitive=False), -) -@click.option( - "--ansible-extras", - default="", - type=str, -) -@click.option("--tls", is_flag=True, help="Launch with TLS configuration") -@click.option("--test", is_flag=True, help="Launch with test configuration") -@click.option("--dev", is_flag=True, help="Shortcut for development mode") -@click.option( - "--release", - default="production", - required=False, - type=click.Choice(["production", "staging", "development"], case_sensitive=False), - help="Choose between production and development release", -) -@click.option( - "--deployment-type", - default="container_stack", - required=False, - type=click.Choice(["container_stack", "single_container"], case_sensitive=False), - help="Choose between container_stack and single_container deployment", -) -@click.option( - "--cert-store-path", - default="/home/om/certs", - required=False, - type=str, - help="Remote path to store and load TLS cert and key", -) -@click.option( - "--upload-tls-cert", - default="", - required=False, - type=str, - help="Local path to TLS cert to upload and store at --cert-store-path", -) -@click.option( - "--upload-tls-key", - default="", - required=False, - type=str, - help="Local path to TLS private key to upload and store at --cert-store-path", -) -@click.option( - "--no-blob-storage", - is_flag=True, - help="Disable blob storage", -) -@click.option( - "--image-name", - default=None, - required=False, - type=str, - help="Image to use for the VM", -) -@click.option( - "--tag", - default=None, - required=False, - type=str, - help="Container image tag to use", -) -@click.option( - "--smtp-username", - default=None, - required=False, - type=str, - help="Username used to auth in email server and enable notification via emails", -) -@click.option( - "--smtp-password", - default=None, - required=False, - type=str, - help="Password used to auth in email server and enable notification via emails", -) -@click.option( - "--smtp-port", - default=None, - required=False, - type=str, - help="Port used by email server to send notification via emails", -) -@click.option( - "--smtp-host", - default=None, - required=False, - type=str, - help="Address used by email server to send notification via emails", -) -@click.option( - "--smtp-sender", - default=None, - required=False, - type=str, - help="Sender email used to deliver PyGrid email notifications.", -) -@click.option( - "--build-src", - default=DEFAULT_BRANCH, - required=False, - type=str, - help="Git branch to use for launch / build operations", -) -@click.option( - "--platform", - default=None, - required=False, - type=str, - help="Run docker with a different platform like linux/arm64", -) -@click.option( - "--verbose", - is_flag=True, - help="Show verbose output", -) -@click.option( - "--trace", - required=False, - type=str, - help="Optional: allow trace to be turned on or off", -) -@click.option( - "--template", - required=False, - default=None, - help="Path or URL to manifest template", -) -@click.option( - "--template-overwrite", - is_flag=True, - help="Force re-downloading of template manifest", -) -@click.option( - "--no-health-checks", - is_flag=True, - help="Turn off auto health checks post node launch", -) -@click.option( - "--set-root-email", - default=None, - required=False, - type=str, - help="Set root email of node", -) -@click.option( - "--set-root-password", - default=None, - required=False, - type=str, - help="Set root password of node", -) -@click.option( - "--azure-resource-group", - default=None, - required=False, - type=str, - help="Azure Resource Group", -) -@click.option( - "--azure-location", - default=None, - required=False, - type=str, - help="Azure Resource Group Location", -) -@click.option( - "--azure-size", - default=None, - required=False, - type=str, - help="Azure VM Size", -) -@click.option( - "--azure-username", - default=None, - required=False, - type=str, - help="Azure VM Username", -) -@click.option( - "--azure-key-path", - default=None, - required=False, - type=str, - help="Azure Key Path", -) -@click.option( - "--azure-repo", - default=None, - required=False, - type=str, - help="Azure Source Repo", -) -@click.option( - "--azure-branch", - default=None, - required=False, - type=str, - help="Azure Source Branch", -) -@click.option( - "--render", - is_flag=True, - help="Render Docker Files", -) -@click.option( - "--no-warnings", - is_flag=True, - help="Enable API warnings on the node.", -) -@click.option( - "--low-side", - is_flag=True, - help="Launch a low side node type else a high side node type", -) -@click.option( - "--set-s3-username", - default=None, - required=False, - type=str, - help="Set root username for s3 blob storage", -) -@click.option( - "--set-s3-password", - default=None, - required=False, - type=str, - help="Set root password for s3 blob storage", -) -@click.option( - "--set-volume-size-limit-mb", - default=1024, - required=False, - type=click.IntRange(1024, 50000), - help="Set the volume size limit (in MBs)", -) -@click.option( - "--association-request-auto-approval", - is_flag=True, - help="Enable auto approval of association requests", -) -def launch(args: tuple[str], **kwargs: Any) -> None: - verb = get_launch_verb() - try: - grammar = parse_grammar(args=args, verb=verb) - verb.load_grammar(grammar=grammar) - except BadGrammar as e: - print(e) - return - - node_name = verb.get_named_term_type(name="node_name") - snake_name = str(node_name.snake_input) - node_type = verb.get_named_term_type(name="node_type") - - # For enclave currently it is only a single container deployment - # This would change when we have side car containers to enclave - if node_type.input == "enclave": - kwargs["deployment_type"] = "single_container" - - compose_src_path = get_compose_src_path( - node_type=node_type, - node_name=snake_name, - template_location=kwargs["template"], - **kwargs, - ) - kwargs["compose_src_path"] = compose_src_path - - try: - update_repo(repo=GIT_REPO(), branch=str(kwargs["build_src"])) - except Exception as e: - print(f"Failed to update repo. {e}") - try: - cmds = create_launch_cmd(verb=verb, kwargs=kwargs) - cmds = [cmds] if isinstance(cmds, str) else cmds - except Exception as e: - print(f"Error: {e}\n\n") - return - - dry_run = bool(kwargs["cmd"]) - - health_checks = not bool(kwargs["no_health_checks"]) - render_only = bool(kwargs["render"]) - - try: - tail = bool(kwargs["tail"]) - verbose = bool(kwargs["verbose"]) - silent = not verbose - if tail: - silent = False - - if render_only: - print( - "Docker Compose Files Rendered: {}".format(kwargs["compose_src_path"]) - ) - return - - execute_commands( - cmds, - dry_run=dry_run, - silent=silent, - compose_src_path=kwargs["compose_src_path"], - node_type=node_type.input, - ) - - host_term = verb.get_named_term_hostgrammar(name="host") - run_health_checks = ( - health_checks and not dry_run and host_term.host == "docker" and silent - ) - - if run_health_checks: - docker_cmds = cast(dict[str, list[str]], cmds) - - # get the first command (cmd1) from docker_cmds which is of the form - # {"": [cmd1, cmd2], "": [cmd3, cmd4]} - (command, *_), *_ = docker_cmds.values() - - match_port = re.search("HTTP_PORT=[0-9]{1,5}", command) - if match_port: - rich.get_console().print( - "\n[bold green]β ‹[bold blue] Checking node API [/bold blue]\t" - ) - port = match_port.group().replace("HTTP_PORT=", "") - - check_status("localhost" + ":" + port, node_name=node_name.snake_input) - - rich.get_console().print( - rich.panel.Panel.fit( - f"✨ To view container logs run [bold green]hagrid logs {node_name.snake_input}[/bold green]\t" - ) - ) - - except Exception as e: - print(f"Error: {e}\n\n") - return - - -def check_errors( - line: str, process: subprocess.Popen, cmd_name: str, progress_bar: Progress -) -> None: - task = progress_bar.tasks[0] - if "Error response from daemon: " in line: - if progress_bar: - progress_bar.update( - 0, - description=f"❌ [bold red]{cmd_name}[/bold red] [{task.completed} / {task.total}]", - refresh=True, - ) - progress_bar.update(0, visible=False) - progress_bar.console.clear_live() - progress_bar.console.quiet = True - progress_bar.stop() - console = rich.get_console() - progress_bar.console.quiet = False - console.print(f"\n\n [red] ERROR [/red]: [bold]{line}[/bold]\n") - process.terminate() - raise Exception - - -def check_pulling(line: str, cmd_name: str, progress_bar: Progress) -> None: - task = progress_bar.tasks[0] - if "Pulling" in line and "fs layer" not in line: - progress_bar.update( - 0, - description=f"[bold]{cmd_name} [{task.completed} / {task.total+1}]", - total=task.total + 1, - refresh=True, - ) - if "Pulled" in line: - progress_bar.update( - 0, - description=f"[bold]{cmd_name} [{task.completed + 1} / {task.total}]", - completed=task.completed + 1, - refresh=True, - ) - if progress_bar.finished: - progress_bar.update( - 0, - description=f"βœ… [bold green]{cmd_name} [{task.completed} / {task.total}]", - refresh=True, - ) - - -def check_building(line: str, cmd_name: str, progress_bar: Progress) -> None: - load_pattern = re.compile( - r"^#.* load build definition from [A-Za-z0-9]+\.dockerfile$", re.IGNORECASE - ) - build_pattern = re.compile( - r"^#.* naming to docker\.io/openmined/.* done$", re.IGNORECASE - ) - task = progress_bar.tasks[0] - - if load_pattern.match(line): - progress_bar.update( - 0, - description=f"[bold]{cmd_name} [{task.completed} / {task.total +1}]", - total=task.total + 1, - refresh=True, - ) - if build_pattern.match(line): - progress_bar.update( - 0, - description=f"[bold]{cmd_name} [{task.completed+1} / {task.total}]", - completed=task.completed + 1, - refresh=True, - ) - - if progress_bar.finished: - progress_bar.update( - 0, - description=f"βœ… [bold green]{cmd_name} [{task.completed} / {task.total}]", - refresh=True, - ) - - -def check_launching(line: str, cmd_name: str, progress_bar: Progress) -> None: - task = progress_bar.tasks[0] - if "Starting" in line: - progress_bar.update( - 0, - description=f" [bold]{cmd_name} [{task.completed} / {task.total+1}]", - total=task.total + 1, - refresh=True, - ) - if "Started" in line: - progress_bar.update( - 0, - description=f" [bold]{cmd_name} [{task.completed + 1} / {task.total}]", - completed=task.completed + 1, - refresh=True, - ) - if progress_bar.finished: - progress_bar.update( - 0, - description=f"βœ… [bold green]{cmd_name} [{task.completed} / {task.total}]", - refresh=True, - ) - - -DOCKER_FUNC_MAP = { - "Pulling": check_pulling, - "Building": check_building, - "Launching": check_launching, -} - - -def read_thread_logs( - progress_bar: Progress, process: subprocess.Popen, queue: Queue, cmd_name: str -) -> None: - line = queue.get() - line = str(line, encoding="utf-8").strip() - - if progress_bar: - check_errors(line, process, cmd_name, progress_bar=progress_bar) - DOCKER_FUNC_MAP[cmd_name](line, cmd_name, progress_bar=progress_bar) - - -def create_thread_logs(process: subprocess.Popen) -> Queue: - def enqueue_output(out: Any, queue: Queue) -> None: - for line in iter(out.readline, b""): - queue.put(line) - out.close() - - queue: Queue = Queue() - thread_1 = Thread(target=enqueue_output, args=(process.stdout, queue)) - thread_2 = Thread(target=enqueue_output, args=(process.stderr, queue)) - - thread_1.daemon = True # thread dies with the program - thread_1.start() - thread_2.daemon = True # thread dies with the program - thread_2.start() - return queue - - -def process_cmd( - cmds: list[str], - node_type: str, - dry_run: bool, - silent: bool, - compose_src_path: str, - progress_bar: Progress | None = None, - cmd_name: str = "", -) -> None: - process_list: list = [] - cwd = compose_src_path - - username, password = ( - extract_username_and_pass(cmds[0]) if len(cmds) > 0 else ("-", "-") - ) - # display VM credentials - console = rich.get_console() - credentials = generate_user_table(username=username, password=password) - if credentials: - console.print(credentials) - - for cmd in cmds: - if dry_run: - print(f"\nRunning:\ncd {cwd}\n", hide_password(cmd=cmd)) - continue - - # use powershell if environment is Windows - cmd_to_exec = ["powershell.exe", "-Command", cmd] if is_windows() else cmd - - try: - if len(cmds) > 1: - process = subprocess.Popen( # nosec - cmd_to_exec, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=cwd, - shell=True, - ) - ip_address = extract_host_ip_from_cmd(cmd) - jupyter_token = extract_jupyter_token(cmd) - process_list.append((ip_address, process, jupyter_token)) - else: - display_jupyter_token(cmd) - if silent: - ON_POSIX = "posix" in sys.builtin_module_names - - process = subprocess.Popen( # nosec - cmd_to_exec, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=cwd, - close_fds=ON_POSIX, - shell=True, - ) - - # Creates two threads to get docker stdout and sterr - logs_queue = create_thread_logs(process=process) - - read_thread_logs(progress_bar, process, logs_queue, cmd_name) - while process.poll() != 0: - while not logs_queue.empty(): - # Read stdout and sterr to check errors or update progress bar. - read_thread_logs( - progress_bar, process, logs_queue, cmd_name - ) - else: - if progress_bar: - progress_bar.stop() - - subprocess.run( # nosec - cmd_to_exec, - shell=True, - cwd=cwd, - ) - except Exception as e: - print(f"Failed to run cmd: {cmd}. {e}") - - if dry_run is False and len(process_list) > 0: - # display VM launch status - display_vm_status(process_list) - - # save vm details as json - save_vm_details_as_json(username, password, process_list) - - -def execute_commands( - cmds: list[str] | dict[str, list[str]], - node_type: str, - compose_src_path: str, - dry_run: bool = False, - silent: bool = False, -) -> None: - """Execute the launch commands and display their status in realtime. - - Args: - cmds (list): list of commands to be executed - dry_run (bool, optional): If `True` only displays cmds to be executed. Defaults to False. - """ - console = rich.get_console() - if isinstance(cmds, dict): - console.print("[bold green]β ‹[bold blue] Launching Containers [/bold blue]\t") - for cmd_name, cmd in cmds.items(): - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - BarColumn(), - TextColumn("[progress.percentage]{task.percentage:.2f}% "), - console=console, - auto_refresh=True, - ) as progress: - if silent: - progress.add_task( - f"[bold green]{cmd_name} Images", - total=0, - ) - process_cmd( - cmds=cmd, - node_type=node_type, - dry_run=dry_run, - silent=silent, - compose_src_path=compose_src_path, - progress_bar=progress, - cmd_name=cmd_name, - ) - else: - process_cmd( - cmds=cmds, - node_type=node_type, - dry_run=dry_run, - silent=silent, - compose_src_path=compose_src_path, - ) - - -def display_vm_status(process_list: list) -> None: - """Display the status of the processes being executed on the VM. - - Args: - process_list (list): list of processes executed. - """ - - # Generate the table showing the status of each process being executed - status_table, process_completed = generate_process_status_table(process_list) - - # Render the live table - with Live(status_table, refresh_per_second=1) as live: - # Loop till all processes have not completed executing - while not process_completed: - status_table, process_completed = generate_process_status_table( - process_list - ) - live.update(status_table) # Update the process status table - - -def display_jupyter_token(cmd: str) -> None: - token = extract_jupyter_token(cmd=cmd) - if token is not None: - print(f"Jupyter Token: {token}") - - -def extract_username_and_pass(cmd: str) -> tuple: - # Extract username - matcher = r"--user (.+?) " - username = re.findall(matcher, cmd) - username = username[0] if len(username) > 0 else None - - # Extract password - matcher = r"ansible_ssh_pass='(.+?)'" - password = re.findall(matcher, cmd) - password = password[0] if len(password) > 0 else None - - return username, password - - -def extract_jupyter_token(cmd: str) -> str | None: - matcher = r"jupyter_token='(.+?)'" - token = re.findall(matcher, cmd) - if len(token) == 1: - return token[0] - return None - - -def hide_password(cmd: str) -> str: - try: - matcher = r"ansible_ssh_pass='(.+?)'" - passwords = re.findall(matcher, cmd) - if len(passwords) > 0: - password = passwords[0] - stars = "*" * 4 - cmd = cmd.replace( - f"ansible_ssh_pass='{password}'", f"ansible_ssh_pass='{stars}'" - ) - return cmd - except Exception as e: - print("Failed to hide password.") - raise e - - -def hide_azure_vm_password(azure_cmd: str) -> str: - try: - matcher = r"admin-password '(.+?)'" - passwords = re.findall(matcher, azure_cmd) - if len(passwords) > 0: - password = passwords[0] - stars = "*" * 4 - azure_cmd = azure_cmd.replace( - f"admin-password '{password}'", f"admin-password '{stars}'" - ) - return azure_cmd - except Exception as e: - print("Failed to hide password.") - raise e - - -class QuestionInputError(Exception): - pass - - -class QuestionInputPathError(Exception): - pass - - -class Question: - def __init__( - self, - var_name: str, - question: str, - kind: str, - default: str | None = None, - cache: bool = False, - options: list[str] | None = None, - ) -> None: - self.var_name = var_name - self.question = question - self.default = default - self.kind = kind - self.cache = cache - self.options = options if options is not None else [] - - def validate(self, value: str) -> str: - value = value.strip() - if self.default is not None and value == "": - return self.default - - if self.kind == "path": - value = os.path.expanduser(value) - if not os.path.exists(value): - error = f"{value} is not a valid path." - if self.default is not None: - error += f" Try {self.default}" - raise QuestionInputPathError(f"{error}") - - if self.kind == "yesno": - if value.lower().startswith("y"): - return "y" - elif value.lower().startswith("n"): - return "n" - else: - raise QuestionInputError(f"{value} is not an yes or no answer") - - if self.kind == "options": - if value in self.options: - return value - first_letter = value.lower()[0] - for option in self.options: - if option.startswith(first_letter): - return option - - raise QuestionInputError( - f"{value} is not one of the options: {self.options}" - ) - - if self.kind == "password": - try: - return validate_password(password=value) - except Exception as e: - raise QuestionInputError(f"Invalid password. {e}") - return value - - -def ask(question: Question, kwargs: dict[str, str]) -> str: - if question.var_name in kwargs and kwargs[question.var_name] is not None: - value = kwargs[question.var_name] - else: - if question.default is not None: - value = click.prompt(question.question, type=str, default=question.default) - elif question.var_name == "password": - value = click.prompt( - question.question, type=str, hide_input=True, confirmation_prompt=True - ) - else: - value = click.prompt(question.question, type=str) - - try: - value = question.validate(value=value) - except QuestionInputError as e: - print(e) - return ask(question=question, kwargs=kwargs) - if question.cache: - arg_cache[question.var_name] = value - - return value - - -def fix_key_permission(private_key_path: str) -> None: - key_permission = oct(stat.S_IMODE(os.stat(private_key_path).st_mode)) - chmod_permission = "400" - octal_permission = f"0o{chmod_permission}" - if key_permission != octal_permission: - print( - f"Fixing key permission: {private_key_path}, setting to {chmod_permission}" - ) - try: - os.chmod(private_key_path, int(octal_permission, 8)) - except Exception as e: - print("Failed to fix key permission", e) - raise e - - -def private_to_public_key(private_key_path: str, temp_path: str, username: str) -> str: - # check key permission - fix_key_permission(private_key_path=private_key_path) - output_path = f"{temp_path}/hagrid_{username}_key.pub" - cmd = f"ssh-keygen -f {private_key_path} -y > {output_path}" - try: - subprocess.check_call(cmd, shell=True) # nosec - except Exception as e: - print("failed to make ssh key", e) - raise e - return output_path - - -def check_azure_authed() -> bool: - cmd = "az account show" - try: - subprocess.check_call(cmd, shell=True, stdout=subprocess.DEVNULL) # nosec - return True - except Exception: # nosec - pass - return False - - -def login_azure() -> bool: - cmd = "az login" - try: - subprocess.check_call(cmd, shell=True, stdout=subprocess.DEVNULL) # nosec - return True - except Exception: # nosec - pass - return False - - -def check_azure_cli_installed() -> bool: - try: - result = subprocess.run( # nosec - ["az", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - if result.returncode != 0: - raise FileNotFoundError("az not installed") - except Exception: # nosec - msg = "\nYou don't appear to have the Azure CLI installed!!! \n\n\ -Please install it and then retry your command.\ -\n\nInstallation Instructions: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli\n" - raise FileNotFoundError(msg) - - return True - - -def check_gcloud_cli_installed() -> bool: - try: - subprocess.call(["gcloud", "version"]) # nosec - print("Gcloud cli installed!") - except FileNotFoundError: - msg = "\nYou don't appear to have the gcloud CLI tool installed! \n\n\ -Please install it and then retry again.\ -\n\nInstallation Instructions: https://cloud.google.com/sdk/docs/install-sdk \n" - raise FileNotFoundError(msg) - - return True - - -def check_aws_cli_installed() -> bool: - try: - result = subprocess.run( # nosec - ["aws", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - if result.returncode != 0: - raise FileNotFoundError("AWS CLI not installed") - except Exception: # nosec - msg = "\nYou don't appear to have the AWS CLI installed! \n\n\ -Please install it and then retry your command.\ -\n\nInstallation Instructions: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html\n" - raise FileNotFoundError(msg) - - return True - - -def check_gcloud_authed() -> bool: - try: - result = subprocess.run( # nosec - ["gcloud", "auth", "print-identity-token"], stdout=subprocess.PIPE - ) - if result.returncode == 0: - return True - except Exception: # nosec - pass - return False - - -def login_gcloud() -> bool: - cmd = "gcloud auth login" - try: - subprocess.check_call(cmd, shell=True, stdout=subprocess.DEVNULL) # nosec - return True - except Exception: # nosec - pass - return False - - -def str_to_bool(bool_str: str | None) -> bool: - result = False - bool_str = str(bool_str).lower() - if bool_str == "true" or bool_str == "1": - result = True - return result - - -ART = str_to_bool(os.environ.get("HAGRID_ART", "True")) - - -def generate_gcloud_key_at_path(key_path: str) -> str: - key_path = os.path.expanduser(key_path) - if os.path.exists(key_path): - raise Exception(f"Can't generate key since path already exists. {key_path}") - else: - # triggers a key check - cmd = "gcloud compute ssh '' --dry-run" - try: - subprocess.check_call(cmd, shell=True) # nosec - except Exception: # nosec - pass - if not os.path.exists(key_path): - raise Exception(f"gcloud failed to generate ssh-key at: {key_path}") - - return key_path - - -def generate_aws_key_at_path(key_path: str, key_name: str) -> str: - key_path = os.path.expanduser(key_path) - if os.path.exists(key_path): - raise Exception(f"Can't generate key since path already exists. {key_path}") - else: - # TODO we need to do differently for powershell. - # Ex: aws ec2 create-key-pair --key-name MyKeyPair --query 'KeyMaterial' - # --output text | out-file -encoding ascii -filepath MyKeyPair.pem - - print(f"Creating AWS key pair with name {key_name} at path {key_path}..") - cmd = f"aws ec2 create-key-pair --key-name {key_name} --query 'KeyMaterial' --output text > {key_path}" - try: - subprocess.check_call(cmd, shell=True) # nosec - subprocess.check_call(f"chmod 400 {key_path}", shell=True) # nosec - except Exception as e: # nosec - print(f"Failed to create key: {e}") - if not os.path.exists(key_path): - raise Exception(f"AWS failed to generate key pair at: {key_path}") - - return key_path - - -def generate_key_at_path(key_path: str) -> str: - key_path = os.path.expanduser(key_path) - if os.path.exists(key_path): - raise Exception(f"Can't generate key since path already exists. {key_path}") - else: - cmd = f"ssh-keygen -N '' -f {key_path}" - try: - subprocess.check_call(cmd, shell=True) # nosec - if not os.path.exists(key_path): - raise Exception(f"Failed to generate ssh-key at: {key_path}") - except Exception as e: - raise e - - return key_path - - -def validate_password(password: str) -> str: - """Validate if the password entered by the user is valid. - - Password length should be between 12 - 123 characters - Passwords must also meet 3 out of the following 4 complexity requirements: - - Have lower characters - - Have upper characters - - Have a digit - - Have a special character - - Args: - password (str): password for the vm - - Returns: - str: password if it is valid - """ - # Validate password length - if len(password) < 12 or len(password) > 123: - raise ValueError("Password length should be between 12 - 123 characters") - - # Valid character types - character_types = { - "upper_case": False, - "lower_case": False, - "digit": False, - "special": False, - } - - for ch in password: - if ch.islower(): - character_types["lower_case"] = True - elif ch.isupper(): - character_types["upper_case"] = True - elif ch.isdigit(): - character_types["digit"] = True - elif ch.isascii(): - character_types["special"] = True - else: - raise ValueError(f"{ch} is not a valid character for password") - - # Validate characters in the password - required_character_type_count = sum( - [int(value) for value in character_types.values()] - ) - - if required_character_type_count >= 3: - return password - - absent_character_types = ", ".join( - char_type for char_type, value in character_types.items() if value is False - ).strip(", ") - - raise ValueError( - f"At least one {absent_character_types} character types must be present" - ) - - -def create_launch_cmd( - verb: GrammarVerb, - kwargs: dict[str, Any], - ignore_docker_version_check: bool | None = False, -) -> str | list[str] | dict[str, list[str]]: - parsed_kwargs: dict[str, Any] = {} - host_term = verb.get_named_term_hostgrammar(name="host") - - host = host_term.host - auth: AuthCredentials | None = None - - tail = bool(kwargs["tail"]) - - parsed_kwargs = {} - - parsed_kwargs["build"] = bool(kwargs["build"]) - - parsed_kwargs["use_blob_storage"] = not bool(kwargs["no_blob_storage"]) - - parsed_kwargs["in_mem_workers"] = bool(kwargs["in_mem_workers"]) - - if parsed_kwargs["use_blob_storage"]: - parsed_kwargs["set_s3_username"] = kwargs["set_s3_username"] - parsed_kwargs["set_s3_password"] = kwargs["set_s3_password"] - parsed_kwargs["set_volume_size_limit_mb"] = kwargs["set_volume_size_limit_mb"] - - parsed_kwargs["association_request_auto_approval"] = str( - kwargs["association_request_auto_approval"] - ) - - parsed_kwargs["node_count"] = ( - int(kwargs["node_count"]) if "node_count" in kwargs else 1 - ) - - if parsed_kwargs["node_count"] > 1 and host not in ["azure"]: - print("\nArgument `node_count` is only supported with `azure`.\n") - else: - # Default to detached mode if running more than one nodes - tail = False if parsed_kwargs["node_count"] > 1 else tail - - headless = bool(kwargs["headless"]) - parsed_kwargs["headless"] = headless - - parsed_kwargs["tls"] = bool(kwargs["tls"]) - parsed_kwargs["test"] = bool(kwargs["test"]) - parsed_kwargs["dev"] = bool(kwargs["dev"]) - - parsed_kwargs["silent"] = not bool(kwargs["verbose"]) - - parsed_kwargs["trace"] = False - if ("trace" not in kwargs or kwargs["trace"] is None) and parsed_kwargs["dev"]: - # default to trace on in dev mode - parsed_kwargs["trace"] = False - elif "trace" in kwargs: - parsed_kwargs["trace"] = str_to_bool(cast(str, kwargs["trace"])) - - parsed_kwargs["release"] = "production" - if "release" in kwargs and kwargs["release"] != "production": - parsed_kwargs["release"] = kwargs["release"] - - # if we use --dev override it - if parsed_kwargs["dev"] is True: - parsed_kwargs["release"] = "development" - - # derive node type - if kwargs["low_side"]: - parsed_kwargs["node_side_type"] = NodeSideType.LOW_SIDE.value - else: - parsed_kwargs["node_side_type"] = NodeSideType.HIGH_SIDE.value - - parsed_kwargs["smtp_username"] = kwargs["smtp_username"] - parsed_kwargs["smtp_password"] = kwargs["smtp_password"] - parsed_kwargs["smtp_port"] = kwargs["smtp_port"] - parsed_kwargs["smtp_host"] = kwargs["smtp_host"] - parsed_kwargs["smtp_sender"] = kwargs["smtp_sender"] - - parsed_kwargs["enable_warnings"] = not kwargs["no_warnings"] - - # choosing deployment type - parsed_kwargs["deployment_type"] = "container_stack" - if "deployment_type" in kwargs and kwargs["deployment_type"] is not None: - parsed_kwargs["deployment_type"] = kwargs["deployment_type"] - - if "cert_store_path" in kwargs: - parsed_kwargs["cert_store_path"] = kwargs["cert_store_path"] - if "upload_tls_cert" in kwargs: - parsed_kwargs["upload_tls_cert"] = kwargs["upload_tls_cert"] - if "upload_tls_key" in kwargs: - parsed_kwargs["upload_tls_key"] = kwargs["upload_tls_key"] - - parsed_kwargs["provision"] = not bool(kwargs["no_provision"]) - - if "image_name" in kwargs and kwargs["image_name"] is not None: - parsed_kwargs["image_name"] = kwargs["image_name"] - else: - parsed_kwargs["image_name"] = "default" - - if parsed_kwargs["dev"] is True: - parsed_kwargs["tag"] = "local" - else: - if "tag" in kwargs and kwargs["tag"] is not None and kwargs["tag"] != "": - parsed_kwargs["tag"] = kwargs["tag"] - else: - parsed_kwargs["tag"] = "latest" - - if "jupyter" in kwargs and kwargs["jupyter"] is not None: - parsed_kwargs["jupyter"] = str_to_bool(cast(str, kwargs["jupyter"])) - else: - parsed_kwargs["jupyter"] = False - - # allows changing docker platform to other cpu architectures like arm64 - parsed_kwargs["platform"] = kwargs["platform"] if "platform" in kwargs else None - - parsed_kwargs["tail"] = tail - - parsed_kwargs["set_root_password"] = ( - kwargs["set_root_password"] if "set_root_password" in kwargs else None - ) - - parsed_kwargs["set_root_email"] = ( - kwargs["set_root_email"] if "set_root_email" in kwargs else None - ) - - parsed_kwargs["template"] = kwargs["template"] if "template" in kwargs else None - parsed_kwargs["template_overwrite"] = bool(kwargs["template_overwrite"]) - - parsed_kwargs["compose_src_path"] = kwargs["compose_src_path"] - - parsed_kwargs["enable_signup"] = str_to_bool(cast(str, kwargs["enable_signup"])) - - # Override template tag with user input tag - if ( - parsed_kwargs["tag"] is not None - and parsed_kwargs["template"] is None - and parsed_kwargs["tag"] not in ["local"] - ): - # third party - from packaging import version - - pattern = r"[0-9].[0-9].[0-9]" - input_tag = parsed_kwargs["tag"] - if ( - not re.match(pattern, input_tag) - and input_tag != "latest" - and input_tag != "beta" - and "b" not in input_tag - ): - raise Exception( - f"Not a valid tag: {parsed_kwargs['tag']}" - + "\nValid tags: latest, beta, beta version(ex: 0.8.2b35),[0-9].[0-9].[0-9]" - ) - - # TODO: we need to redo this so that pypi and docker mappings are in a single - # file inside dev - if parsed_kwargs["tag"] == "latest": - parsed_kwargs["template"] = LATEST_STABLE_SYFT - parsed_kwargs["tag"] = LATEST_STABLE_SYFT - elif parsed_kwargs["tag"] == "beta" or "b" in parsed_kwargs["tag"]: - tag = ( - LATEST_BETA_SYFT - if parsed_kwargs["tag"] == "beta" - else parsed_kwargs["tag"] - ) - - # Currently, manifest_template.yml is only supported for beta versions >= 0.8.2b34 - beta_version = version.parse(tag) - MINIMUM_BETA_VERSION = "0.8.2b34" - if beta_version < version.parse(MINIMUM_BETA_VERSION): - raise Exception( - f"Minimum beta version tag supported is {MINIMUM_BETA_VERSION}" - ) - - # Check if the beta version is available - template_url = f"https://github.com/OpenMined/PySyft/releases/download/v{str(beta_version)}/manifest_template.yml" - response = requests.get(template_url) # nosec - if response.status_code != 200: - raise Exception( - f"Tag {parsed_kwargs['tag']} is not available" - + " \n for download. Please check the available tags at: " - + "\n https://github.com/OpenMined/PySyft/releases" - ) - - parsed_kwargs["template"] = template_url - parsed_kwargs["tag"] = tag - else: - MINIMUM_TAG_VERSION = version.parse("0.8.0") - tag = version.parse(parsed_kwargs["tag"]) - if tag < MINIMUM_TAG_VERSION: - raise Exception( - f"Minimum supported stable tag version is {MINIMUM_TAG_VERSION}" - ) - parsed_kwargs["template"] = parsed_kwargs["tag"] - - if host in ["docker"] and parsed_kwargs["template"] and host is not None: - # Setup the files from the manifest_template.yml - kwargs = setup_from_manifest_template( - host_type=host, - deployment_type=parsed_kwargs["deployment_type"], - template_location=parsed_kwargs["template"], - overwrite=parsed_kwargs["template_overwrite"], - verbose=kwargs["verbose"], - ) - - parsed_kwargs.update(kwargs) - - if host in ["docker"]: - # Check docker service status - if not ignore_docker_version_check: - check_docker_service_status() - - # Check grid docker versions - if not ignore_docker_version_check: - check_grid_docker(display=True, output_in_text=True) - - if not ignore_docker_version_check: - version = check_docker_version() - else: - version = "n/a" - - if version: - # If the user is using docker desktop (OSX/Windows), check to make sure there's enough RAM. - # If the user is using Linux this isn't an issue because Docker scales to the avaialble RAM, - # but on Docker Desktop it defaults to 2GB which isn't enough. - dd_memory = docker_desktop_memory() - if dd_memory < 8192 and dd_memory != -1: - raise Exception( - "You appear to be using Docker Desktop but don't have " - "enough memory allocated. It appears you've configured " - f"Memory:{dd_memory} MB when 8192MB (8GB) is required. " - f"Please open Docker Desktop Preferences panel and set Memory" - f" to 8GB or higher. \n\n" - f"\tOSX Help: https://docs.docker.com/desktop/mac/\n" - f"\tWindows Help: https://docs.docker.com/desktop/windows/\n\n" - f"Then re-run your hagrid command.\n\n" - f"If you see this warning on Linux then something isn't right. " - f"Please file a Github Issue on PySyft's Github.\n\n" - f"Alternatively in case no more memory could be allocated, " - f"you can run hagrid on the cloud with GitPod by visiting " - f"https://gitpod.io/#https://github.com/OpenMined/PySyft." - ) - - if is_windows() and not DEPENDENCIES["wsl"]: - raise Exception( - "You must install wsl2 for Windows to use HAGrid.\n" - "In PowerShell or Command Prompt type:\n> wsl --install\n\n" - "Read more here: https://docs.microsoft.com/en-us/windows/wsl/install" - ) - - return create_launch_docker_cmd( - verb=verb, - docker_version=version, - tail=tail, - kwargs=parsed_kwargs, - silent=parsed_kwargs["silent"], - ) - - elif host in ["azure"]: - check_azure_cli_installed() - - while not check_azure_authed(): - print("You need to log into Azure") - login_azure() - - if DEPENDENCIES["ansible-playbook"]: - resource_group = ask( - question=Question( - var_name="azure_resource_group", - question="What resource group name do you want to use (or create)?", - default=arg_cache["azure_resource_group"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - location = ask( - question=Question( - var_name="azure_location", - question="If this is a new resource group what location?", - default=arg_cache["azure_location"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - size = ask( - question=Question( - var_name="azure_size", - question="What size machine?", - default=arg_cache["azure_size"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - username = ask( - question=Question( - var_name="azure_username", - question="What do you want the username for the VM to be?", - default=arg_cache["azure_username"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - parsed_kwargs["auth_type"] = ask( - question=Question( - var_name="auth_type", - question="Do you want to login with a key or password", - default=arg_cache["auth_type"], - kind="option", - options=["key", "password"], - cache=True, - ), - kwargs=kwargs, - ) - - key_path = None - if parsed_kwargs["auth_type"] == "key": - key_path_question = Question( - var_name="azure_key_path", - question=f"Absolute path of the private key to access {username}@{host}?", - default=arg_cache["azure_key_path"], - kind="path", - cache=True, - ) - try: - key_path = ask( - key_path_question, - kwargs=kwargs, - ) - except QuestionInputPathError as e: - print(e) - key_path = str(e).split("is not a valid path")[0].strip() - - create_key_question = Question( - var_name="azure_key_path", - question=f"Key {key_path} does not exist. Do you want to create it? (y/n)", - default="y", - kind="yesno", - ) - create_key = ask( - create_key_question, - kwargs=kwargs, - ) - if create_key == "y": - key_path = generate_key_at_path(key_path=key_path) - else: - raise QuestionInputError( - "Unable to create VM without a private key" - ) - elif parsed_kwargs["auth_type"] == "password": - auto_generate_password = ask( - question=Question( - var_name="auto_generate_password", - question="Do you want to auto-generate the password? (y/n)", - kind="yesno", - ), - kwargs=kwargs, - ) - if auto_generate_password == "y": # nosec - parsed_kwargs["password"] = generate_sec_random_password(length=16) - elif auto_generate_password == "n": # nosec - parsed_kwargs["password"] = ask( - question=Question( - var_name="password", - question=f"Password for {username}@{host}?", - kind="password", - ), - kwargs=kwargs, - ) - - repo = ask( - Question( - var_name="azure_repo", - question="Repo to fetch source from?", - default=arg_cache["azure_repo"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - branch = ask( - Question( - var_name="azure_branch", - question="Branch to monitor for updates?", - default=arg_cache["azure_branch"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - use_branch(branch=branch) - - password = parsed_kwargs.get("password") - - auth = AuthCredentials( - username=username, key_path=key_path, password=password - ) - - if not auth.valid: - raise Exception(f"Login Credentials are not valid. {auth}") - - return create_launch_azure_cmd( - verb=verb, - resource_group=resource_group, - location=location, - size=size, - username=username, - password=password, - key_path=key_path, - repo=repo, - branch=branch, - auth=auth, - ansible_extras=kwargs["ansible_extras"], - kwargs=parsed_kwargs, - ) - else: - errors = [] - if not DEPENDENCIES["ansible-playbook"]: - errors.append("ansible-playbook") - msg = "\nERROR!!! MISSING DEPENDENCY!!!" - msg += f"\n\nLaunching a Cloud VM requires: {' '.join(errors)}" - msg += "\n\nPlease follow installation instructions: " - msg += "https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#" - msg += "\n\nNote: we've found the 'conda' based installation instructions to work best" - msg += " (e.g. something lke 'conda install -c conda-forge ansible'). " - msg += "The pip based instructions seem to be a bit buggy if you're using a conda environment" - msg += "\n" - raise MissingDependency(msg) - - elif host in ["gcp"]: - check_gcloud_cli_installed() - - while not check_gcloud_authed(): - print("You need to log into Google Cloud") - login_gcloud() - - if DEPENDENCIES["ansible-playbook"]: - project_id = ask( - question=Question( - var_name="gcp_project_id", - question="What PROJECT ID do you want to use?", - default=arg_cache["gcp_project_id"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - zone = ask( - question=Question( - var_name="gcp_zone", - question="What zone do you want your VM in?", - default=arg_cache["gcp_zone"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - machine_type = ask( - question=Question( - var_name="gcp_machine_type", - question="What size machine?", - default=arg_cache["gcp_machine_type"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - username = ask( - question=Question( - var_name="gcp_username", - question="What is your shell username?", - default=arg_cache["gcp_username"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - key_path_question = Question( - var_name="gcp_key_path", - question=f"Private key to access user@{host}?", - default=arg_cache["gcp_key_path"], - kind="path", - cache=True, - ) - try: - key_path = ask( - key_path_question, - kwargs=kwargs, - ) - except QuestionInputPathError as e: - print(e) - key_path = str(e).split("is not a valid path")[0].strip() - - create_key_question = Question( - var_name="gcp_key_path", - question=f"Key {key_path} does not exist. Do you want gcloud to make it? (y/n)", - default="y", - kind="yesno", - ) - create_key = ask( - create_key_question, - kwargs=kwargs, - ) - if create_key == "y": - key_path = generate_gcloud_key_at_path(key_path=key_path) - else: - raise QuestionInputError( - "Unable to create VM without a private key" - ) - - repo = ask( - Question( - var_name="gcp_repo", - question="Repo to fetch source from?", - default=arg_cache["gcp_repo"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - branch = ask( - Question( - var_name="gcp_branch", - question="Branch to monitor for updates?", - default=arg_cache["gcp_branch"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - use_branch(branch=branch) - - auth = AuthCredentials(username=username, key_path=key_path) - - return create_launch_gcp_cmd( - verb=verb, - project_id=project_id, - zone=zone, - machine_type=machine_type, - repo=repo, - auth=auth, - branch=branch, - ansible_extras=kwargs["ansible_extras"], - kwargs=parsed_kwargs, - ) - else: - errors = [] - if not DEPENDENCIES["ansible-playbook"]: - errors.append("ansible-playbook") - msg = "\nERROR!!! MISSING DEPENDENCY!!!" - msg += f"\n\nLaunching a Cloud VM requires: {' '.join(errors)}" - msg += "\n\nPlease follow installation instructions: " - msg += "https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#" - msg += "\n\nNote: we've found the 'conda' based installation instructions to work best" - msg += " (e.g. something lke 'conda install -c conda-forge ansible'). " - msg += "The pip based instructions seem to be a bit buggy if you're using a conda environment" - msg += "\n" - raise MissingDependency(msg) - - elif host in ["aws"]: - check_aws_cli_installed() - - if DEPENDENCIES["ansible-playbook"]: - aws_region = ask( - question=Question( - var_name="aws_region", - question="In what region do you want to deploy the EC2 instance?", - default=arg_cache["aws_region"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - aws_security_group_name = ask( - question=Question( - var_name="aws_security_group_name", - question="Name of the security group to be created?", - default=arg_cache["aws_security_group_name"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - aws_security_group_cidr = ask( - question=Question( - var_name="aws_security_group_cidr", - question="What IP addresses to allow for incoming network traffic? Please use CIDR notation", - default=arg_cache["aws_security_group_cidr"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - ec2_instance_type = ask( - question=Question( - var_name="aws_ec2_instance_type", - question="What EC2 instance type do you want to deploy?", - default=arg_cache["aws_ec2_instance_type"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - aws_key_name = ask( - question=Question( - var_name="aws_key_name", - question="Enter the name of the key pair to use to connect to the EC2 instance", - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - key_path_qn_str = ( - "Please provide the path of the private key to connect to the instance" - ) - key_path_qn_str += " (if it does not exist, this path corresponds to " - key_path_qn_str += "where you want to store the key upon creation)" - key_path_question = Question( - var_name="aws_key_path", - question=key_path_qn_str, - kind="path", - cache=True, - ) - try: - key_path = ask( - key_path_question, - kwargs=kwargs, - ) - except QuestionInputPathError as e: - print(e) - key_path = str(e).split("is not a valid path")[0].strip() - - create_key_question = Question( - var_name="aws_key_path", - question=f"Key {key_path} does not exist. Do you want AWS to make it? (y/n)", - default="y", - kind="yesno", - ) - create_key = ask( - create_key_question, - kwargs=kwargs, - ) - if create_key == "y": - key_path = generate_aws_key_at_path( - key_path=key_path, key_name=aws_key_name - ) - else: - raise QuestionInputError( - "Unable to create EC2 instance without key" - ) - - repo = ask( - Question( - var_name="aws_repo", - question="Repo to fetch source from?", - default=arg_cache["aws_repo"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - branch = ask( - Question( - var_name="aws_branch", - question="Branch to monitor for updates?", - default=arg_cache["aws_branch"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - use_branch(branch=branch) - - username = arg_cache["aws_ec2_instance_username"] - auth = AuthCredentials(username=username, key_path=key_path) - - return create_launch_aws_cmd( - verb=verb, - region=aws_region, - ec2_instance_type=ec2_instance_type, - security_group_name=aws_security_group_name, - aws_security_group_cidr=aws_security_group_cidr, - key_path=key_path, - key_name=aws_key_name, - repo=repo, - branch=branch, - ansible_extras=kwargs["ansible_extras"], - kwargs=parsed_kwargs, - ami_id=arg_cache["aws_image_id"], - username=username, - auth=auth, - ) - - else: - errors = [] - if not DEPENDENCIES["ansible-playbook"]: - errors.append("ansible-playbook") - msg = "\nERROR!!! MISSING DEPENDENCY!!!" - msg += f"\n\nLaunching a Cloud VM requires: {' '.join(errors)}" - msg += "\n\nPlease follow installation instructions: " - msg += "https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#" - msg += "\n\nNote: we've found the 'conda' based installation instructions to work best" - msg += " (e.g. something lke 'conda install -c conda-forge ansible'). " - msg += "The pip based instructions seem to be a bit buggy if you're using a conda environment" - msg += "\n" - raise MissingDependency(msg) - else: - if DEPENDENCIES["ansible-playbook"]: - if host != "localhost": - parsed_kwargs["username"] = ask( - question=Question( - var_name="username", - question=f"Username for {host} with sudo privledges?", - default=arg_cache["username"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - parsed_kwargs["auth_type"] = ask( - question=Question( - var_name="auth_type", - question="Do you want to login with a key or password", - default=arg_cache["auth_type"], - kind="option", - options=["key", "password"], - cache=True, - ), - kwargs=kwargs, - ) - if parsed_kwargs["auth_type"] == "key": - parsed_kwargs["key_path"] = ask( - question=Question( - var_name="key_path", - question=f"Private key to access {parsed_kwargs['username']}@{host}?", - default=arg_cache["key_path"], - kind="path", - cache=True, - ), - kwargs=kwargs, - ) - elif parsed_kwargs["auth_type"] == "password": - parsed_kwargs["password"] = ask( - question=Question( - var_name="password", - question=f"Password for {parsed_kwargs['username']}@{host}?", - kind="password", - ), - kwargs=kwargs, - ) - - parsed_kwargs["repo"] = ask( - question=Question( - var_name="repo", - question="Repo to fetch source from?", - default=arg_cache["repo"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - parsed_kwargs["branch"] = ask( - Question( - var_name="branch", - question="Branch to monitor for updates?", - default=arg_cache["branch"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - auth = None - if host != "localhost": - if parsed_kwargs["auth_type"] == "key": - auth = AuthCredentials( - username=parsed_kwargs["username"], - key_path=parsed_kwargs["key_path"], - ) - else: - auth = AuthCredentials( - username=parsed_kwargs["username"], - key_path=parsed_kwargs["password"], - ) - if not auth.valid: - raise Exception(f"Login Credentials are not valid. {auth}") - parsed_kwargs["ansible_extras"] = kwargs["ansible_extras"] - return create_launch_custom_cmd(verb=verb, auth=auth, kwargs=parsed_kwargs) - else: - errors = [] - if not DEPENDENCIES["ansible-playbook"]: - errors.append("ansible-playbook") - raise MissingDependency( - f"Launching a Custom VM requires: {' '.join(errors)}" - ) - - host_options = ", ".join(allowed_hosts) - raise MissingDependency( - f"Launch requires a correct host option, try: {host_options}" - ) - - -def pull_command(cmd: str, kwargs: dict[str, Any]) -> list[str]: - pull_cmd = str(cmd) - if kwargs["release"] == "production": - pull_cmd += " --file docker-compose.yml" - else: - pull_cmd += " --file docker-compose.pull.yml" - pull_cmd += " pull --ignore-pull-failures" # ignore missing version from Dockerhub - return [pull_cmd] - - -def build_command(cmd: str) -> list[str]: - build_cmd = str(cmd) - build_cmd += " --file docker-compose.build.yml" - build_cmd += " build" - return [build_cmd] - - -def deploy_command(cmd: str, tail: bool, dev_mode: bool) -> list[str]: - up_cmd = str(cmd) - up_cmd += " --file docker-compose.dev.yml" if dev_mode else "" - up_cmd += " up" - if not tail: - up_cmd += " -d" - return [up_cmd] - - -def create_launch_docker_cmd( - verb: GrammarVerb, - docker_version: str, - kwargs: dict[str, Any], - tail: bool = True, - silent: bool = False, -) -> dict[str, list[str]]: - host_term = verb.get_named_term_hostgrammar(name="host") - node_name = verb.get_named_term_type(name="node_name") - node_type = verb.get_named_term_type(name="node_type") - - snake_name = str(node_name.snake_input) - tag = name_tag(name=str(node_name.input)) - - if ART and not silent: - hagrid() - - print( - "Launching a PyGrid " - + str(node_type.input).capitalize() - + " node on port " - + str(host_term.free_port) - + "!\n" - ) - - version_string = kwargs["tag"] - version_hash = "dockerhub" - build = kwargs["build"] - - # if in development mode, generate a version_string which is either - # the one you inputed concatenated with -dev or the contents of the VERSION file - version = GRID_SRC_VERSION() - if "release" in kwargs and kwargs["release"] == "development": - # force version to have -dev at the end in dev mode - # during development we can use the latest beta version - if version_string is None: - version_string = version[0] - version_string += "-dev" - version_hash = version[1] - build = True - else: - # whereas if in production mode and tag == "local" use the local VERSION file - # or if its not set somehow, which should never happen, use stable - # otherwise use the kwargs["tag"] from above - - # during production the default would be stable - if version_string == "local": - # this can be used in VMs in production to auto update from src - version_string = version[0] - version_hash = version[1] - build = True - elif version_string is None: - version_string = "latest" - - if platform.uname().machine.lower() in ["x86_64", "amd64"]: - docker_platform = "linux/amd64" - else: - docker_platform = "linux/arm64" - - if "platform" in kwargs and kwargs["platform"] is not None: - docker_platform = kwargs["platform"] - - if kwargs["template"]: - _, template_hash = get_template_yml(kwargs["template"]) - template_dir = manifest_cache_path(template_hash) - template_grid_dir = f"{template_dir}/packages/grid" - else: - template_grid_dir = GRID_SRC_PATH() - - compose_src_path = kwargs["compose_src_path"] - if not compose_src_path: - compose_src_path = get_compose_src_path( - node_type=node_type, - node_name=snake_name, - template_location=kwargs["template"], - **kwargs, - ) - - default_env = f"{template_grid_dir}/default.env" - if not os.path.exists(default_env): - # old path - default_env = f"{template_grid_dir}/.env" - default_envs = {} - with open(default_env) as f: - for line in f.readlines(): - if "=" in line: - parts = line.strip().split("=") - key = parts[0] - value = "" - if len(parts) > 1: - value = parts[1] - default_envs[key] = value - - single_container_mode = kwargs["deployment_type"] == "single_container" - in_mem_workers = kwargs.get("in_mem_workers") - smtp_username = kwargs.get("smtp_username") - smtp_sender = kwargs.get("smtp_sender") - smtp_password = kwargs.get("smtp_password") - smtp_port = kwargs.get("smtp_port") - if smtp_port is None or smtp_port == "": - smtp_port = int(default_envs["SMTP_PORT"]) - smtp_host = kwargs.get("smtp_host") - - print(" - NAME: " + str(snake_name)) - print(" - TEMPLATE DIR: " + template_grid_dir) - if compose_src_path: - print(" - COMPOSE SOURCE: " + compose_src_path) - print(" - RELEASE: " + f'{kwargs["node_side_type"]}-{kwargs["release"]}') - print(" - DEPLOYMENT:", kwargs["deployment_type"]) - print(" - ARCH: " + docker_platform) - print(" - TYPE: " + str(node_type.input)) - print(" - DOCKER_TAG: " + version_string) - if version_hash != "dockerhub": - print(" - GIT_HASH: " + version_hash) - print(" - HAGRID_VERSION: " + get_version_string()) - if EDITABLE_MODE: - print(" - HAGRID_REPO_SHA: " + commit_hash()) - print(" - PORT: " + str(host_term.free_port)) - print(" - DOCKER COMPOSE: " + docker_version) - print(" - IN-MEMORY WORKERS: " + str(in_mem_workers)) - print("\n") - - use_blob_storage = ( - False - if str(node_type.input) in ["network", "gateway"] - else bool(kwargs["use_blob_storage"]) - ) - - # use a docker volume - host_path = "credentials-data" - - # # in development use a folder mount - # if kwargs.get("release", "") == "development": - # RELATIVE_PATH = "" - # # if EDITABLE_MODE: - # # RELATIVE_PATH = "../" - # # we might need to change this for the hagrid template mode - # host_path = f"{RELATIVE_PATH}./data/storage/{snake_name}" - - envs = { - "RELEASE": "production", - "COMPOSE_DOCKER_CLI_BUILD": 1, - "DOCKER_BUILDKIT": 1, - "HTTP_PORT": int(host_term.free_port), - "HTTPS_PORT": int(host_term.free_port_tls), - "TRAEFIK_TAG": str(tag), - "NODE_NAME": str(snake_name), - "NODE_TYPE": str(node_type.input), - "TRAEFIK_PUBLIC_NETWORK_IS_EXTERNAL": "False", - "VERSION": version_string, - "VERSION_HASH": version_hash, - "USE_BLOB_STORAGE": str(use_blob_storage), - "FRONTEND_TARGET": "grid-ui-production", - "STACK_API_KEY": str( - generate_sec_random_password(length=48, special_chars=False) - ), - "CREDENTIALS_VOLUME": host_path, - "NODE_SIDE_TYPE": kwargs["node_side_type"], - "SINGLE_CONTAINER_MODE": single_container_mode, - "INMEMORY_WORKERS": in_mem_workers, - } - - if smtp_host and smtp_port and smtp_username and smtp_password: - envs["SMTP_HOST"] = smtp_host - envs["SMTP_PORT"] = smtp_port - envs["SMTP_USERNAME"] = smtp_username - envs["SMTP_PASSWORD"] = smtp_password - envs["EMAIL_SENDER"] = smtp_sender - - if "trace" in kwargs and kwargs["trace"] is True: - envs["TRACE"] = "True" - envs["JAEGER_HOST"] = "host.docker.internal" - envs["JAEGER_PORT"] = int( - find_available_port(host="localhost", port=14268, search=True) - ) - - if "association_request_auto_approval" in kwargs: - envs["ASSOCIATION_REQUEST_AUTO_APPROVAL"] = kwargs[ - "association_request_auto_approval" - ] - - if "enable_warnings" in kwargs: - envs["ENABLE_WARNINGS"] = kwargs["enable_warnings"] - - if "platform" in kwargs and kwargs["platform"] is not None: - envs["DOCKER_DEFAULT_PLATFORM"] = docker_platform - - if "tls" in kwargs and kwargs["tls"] is True and len(kwargs["cert_store_path"]) > 0: - envs["TRAEFIK_TLS_CERTS"] = kwargs["cert_store_path"] - - if ( - "tls" in kwargs - and kwargs["tls"] is True - and "test" in kwargs - and kwargs["test"] is True - ): - envs["IGNORE_TLS_ERRORS"] = "True" - - if "test" in kwargs and kwargs["test"] is True: - envs["SWFS_VOLUME_SIZE_LIMIT_MB"] = "100" # GitHub CI is small - - if kwargs.get("release", "") == "development": - envs["RABBITMQ_MANAGEMENT"] = "-management" - - # currently we only have a domain frontend for dev mode - if kwargs.get("release", "") == "development" and ( - str(node_type.input) not in ["network", "gateway"] - ): - envs["FRONTEND_TARGET"] = "grid-ui-development" - - if "set_root_password" in kwargs and kwargs["set_root_password"] is not None: - envs["DEFAULT_ROOT_PASSWORD"] = kwargs["set_root_password"] - - if "set_root_email" in kwargs and kwargs["set_root_email"] is not None: - envs["DEFAULT_ROOT_EMAIL"] = kwargs["set_root_email"] - - if "set_s3_username" in kwargs and kwargs["set_s3_username"] is not None: - envs["S3_ROOT_USER"] = kwargs["set_s3_username"] - - if "set_s3_password" in kwargs and kwargs["set_s3_password"] is not None: - envs["S3_ROOT_PWD"] = kwargs["set_s3_password"] - - if ( - "set_volume_size_limit_mb" in kwargs - and kwargs["set_volume_size_limit_mb"] is not None - ): - envs["SWFS_VOLUME_SIZE_LIMIT_MB"] = kwargs["set_volume_size_limit_mb"] - - if "release" in kwargs: - envs["RELEASE"] = kwargs["release"] - - if "enable_signup" in kwargs: - envs["ENABLE_SIGNUP"] = kwargs["enable_signup"] - - cmd = "" - args = [] - for k, v in envs.items(): - if is_windows(): - # powershell envs - quoted = f"'{v}'" if not isinstance(v, int) else v - args.append(f"$env:{k}={quoted}") - else: - args.append(f"{k}={v}") - if is_windows(): - cmd += "; ".join(args) - cmd += "; " - else: - cmd += " ".join(args) - - cmd += " docker compose -p " + snake_name - - # new docker compose regression work around - # default_env = os.path.expanduser("~/.hagrid/app/.env") - - default_envs.update(envs) - - # env file path - env_file_path = compose_src_path + "/.env" - - # Render templates if creating stack from the manifest_template.yml - if kwargs["template"] and host_term.host is not None: - # If release is development, update relative path - # if EDITABLE_MODE: - # default_envs["RELATIVE_PATH"] = "../" - - render_templates( - node_name=snake_name, - deployment_type=kwargs["deployment_type"], - template_location=kwargs["template"], - env_vars=default_envs, - host_type=host_term.host, - ) - - try: - env_file = "" - for k, v in default_envs.items(): - env_file += f"{k}={v}\n" - - with open(env_file_path, "w") as f: - f.write(env_file) - - # cmd += f" --env-file {env_file_path}" - except Exception: # nosec - pass - - if single_container_mode: - cmd += " --profile worker" - else: - cmd += " --profile backend" - cmd += " --profile proxy" - cmd += " --profile mongo" - - if str(node_type.input) in ["network", "gateway"]: - cmd += " --profile network" - - if use_blob_storage: - cmd += " --profile blob-storage" - - # no frontend container so expect bad gateway on the / route - if not bool(kwargs["headless"]): - cmd += " --profile frontend" - - if "trace" in kwargs and kwargs["trace"]: - cmd += " --profile telemetry" - - final_commands = {} - final_commands["Pulling"] = pull_command(cmd, kwargs) - - cmd += " --file docker-compose.yml" - if "tls" in kwargs and kwargs["tls"] is True: - cmd += " --file docker-compose.tls.yml" - if "test" in kwargs and kwargs["test"] is True: - cmd += " --file docker-compose.test.yml" - - if build: - my_build_command = build_command(cmd) - final_commands["Building"] = my_build_command - - dev_mode = kwargs.get("dev", False) - final_commands["Launching"] = deploy_command(cmd, tail, dev_mode) - return final_commands - - -def create_launch_vagrant_cmd(verb: GrammarVerb) -> str: - host_term = verb.get_named_term_hostgrammar(name="host") - node_name = verb.get_named_term_type(name="node_name") - node_type = verb.get_named_term_type(name="node_type") - - snake_name = str(node_name.snake_input) - - if ART: - hagrid() - - print( - "Launching a " - + str(node_type.input) - + " PyGrid node on port " - + str(host_term.port) - + "!\n" - ) - - print(" - TYPE: " + str(node_type.input)) - print(" - NAME: " + str(snake_name)) - print(" - PORT: " + str(host_term.port)) - # print(" - VAGRANT: " + "1") - # print(" - VIRTUALBOX: " + "1") - print("\n") - - cmd = "" - cmd += 'ANSIBLE_ARGS="' - cmd += f"-e 'node_name={snake_name}'" - cmd += f"-e 'node_type={node_type.input}'" - cmd += '" ' - cmd += "vagrant up --provision" - cmd = "cd " + GRID_SRC_PATH() + ";" + cmd - return cmd - - -def get_or_make_resource_group(resource_group: str, location: str = "westus") -> None: - cmd = f"az group show --resource-group {resource_group}" - exists = True - try: - subprocess.check_call(cmd, shell=True) # nosec - except Exception: # nosec - # group doesn't exist so lets create it - exists = False - - if not exists: - cmd = f"az group create -l {location} -n {resource_group}" - try: - print(f"Creating resource group.\nRunning: {cmd}") - subprocess.check_call(cmd, shell=True) # nosec - except Exception as e: - raise Exception( - f"Unable to create resource group {resource_group} @ {location}. {e}" - ) - - -def extract_host_ip(stdout: bytes) -> str | None: - output = stdout.decode("utf-8") - - try: - j = json.loads(output) - if "publicIpAddress" in j: - return str(j["publicIpAddress"]) - except Exception: # nosec - matcher = r'publicIpAddress":\s+"(.+)"' - ips = re.findall(matcher, output) - if len(ips) > 0: - return ips[0] - - return None - - -def get_vm_host_ips(node_name: str, resource_group: str) -> list | None: - cmd = f"az vm list-ip-addresses -g {resource_group} --query " - cmd += f""""[?starts_with(virtualMachine.name, '{node_name}')]""" - cmd += '''.virtualMachine.network.publicIpAddresses[0].ipAddress"''' - output = subprocess.check_output(cmd, shell=True) # nosec - try: - host_ips = json.loads(output) - return host_ips - except Exception as e: - print(f"Failed to extract ips: {e}") - - return None - - -def is_valid_ip(host_or_ip: str) -> bool: - matcher = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" - ips = re.findall(matcher, host_or_ip.strip()) - if len(ips) == 1: - return True - return False - - -def extract_host_ip_gcp(stdout: bytes) -> str | None: - output = stdout.decode("utf-8") - - try: - matcher = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" - ips = re.findall(matcher, output) - if len(ips) == 2: - return ips[1] - except Exception: # nosec - pass - - return None - - -def extract_host_ip_from_cmd(cmd: str) -> str | None: - try: - matcher = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" - ips = re.findall(matcher, cmd) - if ips: - return ips[0] - except Exception: # nosec - pass - - return None - - -def check_ip_for_ssh( - host_ip: str, timeout: int = 600, wait_time: int = 5, silent: bool = False -) -> bool: - if not silent: - print(f"Checking VM at {host_ip} is up") - checks = int(timeout / wait_time) # 10 minutes in 5 second chunks - first_run = True - while checks > 0: - checks -= 1 - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(wait_time) - result = sock.connect_ex((host_ip, 22)) - sock.close() - if result == 0: - if not silent: - print(f"VM at {host_ip} is up!") - return True - else: - if first_run: - if not silent: - print("Waiting for VM to start", end="", flush=True) - first_run = False - else: - if not silent: - print(".", end="", flush=True) - except Exception: # nosec - pass - return False - - -def create_aws_security_group( - security_group_name: str, region: str, snake_name: str -) -> str: - sg_description = f"{snake_name} security group" - create_cmd = f"aws ec2 create-security-group --group-name {security_group_name} " - create_cmd += f'--region {region} --description "{sg_description}" ' - sg_output = subprocess.check_output( # nosec - create_cmd, - shell=True, - ) - sg_output_dict = json.loads(sg_output) - if "GroupId" in sg_output_dict: - return sg_output_dict["GroupId"] - - return "" - - -def open_port_aws( - security_group_name: str, port_no: int, cidr: str, region: str -) -> None: - cmd = f"aws ec2 authorize-security-group-ingress --group-name {security_group_name} --protocol tcp " - cmd += f"--port {port_no} --cidr {cidr} --region {region}" - subprocess.check_call( # nosec - cmd, - shell=True, - ) - - -def extract_instance_ids_aws(stdout: bytes) -> list: - output = stdout.decode("utf-8") - output_dict = json.loads(output) - instance_ids: list = [] - if "Instances" in output_dict: - for ec2_instance_metadata in output_dict["Instances"]: - if "InstanceId" in ec2_instance_metadata: - instance_ids.append(ec2_instance_metadata["InstanceId"]) - - return instance_ids - - -def get_host_ips_given_instance_ids( - instance_ids: list, timeout: int = 600, wait_time: int = 10 -) -> list: - checks = int(timeout / wait_time) # 10 minutes in 10 second chunks - instance_ids_str = " ".join(instance_ids) - cmd = f"aws ec2 describe-instances --instance-ids {instance_ids_str}" - cmd += " --query 'Reservations[*].Instances[*].{StateName:State.Name,PublicIpAddress:PublicIpAddress}'" - cmd += " --output json" - while checks > 0: - checks -= 1 - time.sleep(wait_time) - desc_ec2_output = subprocess.check_output(cmd, shell=True) # nosec - instances_output_json = json.loads(desc_ec2_output.decode("utf-8")) - host_ips: list = [] - all_instances_running = True - for reservation in instances_output_json: - for instance_metadata in reservation: - if instance_metadata["StateName"] != "running": - all_instances_running = False - break - else: - host_ips.append(instance_metadata["PublicIpAddress"]) - if all_instances_running: - return host_ips - # else, wait another wait_time seconds and try again - - return [] - - -def make_aws_ec2_instance( - ami_id: str, ec2_instance_type: str, key_name: str, security_group_name: str -) -> list: - # From the docs: "For security groups in a nondefault VPC, you must specify the security group ID". - # Right now, since we're using default VPC, we can use security group name instead of ID. - - ebs_size = 200 # gb - cmd = f"aws ec2 run-instances --image-id {ami_id} --count 1 --instance-type {ec2_instance_type} " - cmd += f"--key-name {key_name} --security-groups {security_group_name} " - tmp_cmd = rf"[{{\"DeviceName\":\"/dev/sdf\",\"Ebs\":{{\"VolumeSize\":{ebs_size},\"DeleteOnTermination\":false}}}}]" - cmd += f'--block-device-mappings "{tmp_cmd}"' - - host_ips: list = [] - try: - print(f"Creating EC2 instance.\nRunning: {cmd}") - create_ec2_output = subprocess.check_output(cmd, shell=True) # nosec - instance_ids = extract_instance_ids_aws(create_ec2_output) - host_ips = get_host_ips_given_instance_ids(instance_ids=instance_ids) - except Exception as e: - print("failed", e) - - if not (host_ips): - raise Exception("Failed to create EC2 instance(s) or get public ip(s)") - - return host_ips - - -def create_launch_aws_cmd( - verb: GrammarVerb, - region: str, - ec2_instance_type: str, - security_group_name: str, - aws_security_group_cidr: str, - key_name: str, - key_path: str, - ansible_extras: str, - kwargs: dict[str, Any], - repo: str, - branch: str, - ami_id: str, - username: str, - auth: AuthCredentials, -) -> list[str]: - node_name = verb.get_named_term_type(name="node_name") - snake_name = str(node_name.snake_input) - create_aws_security_group(security_group_name, region, snake_name) - open_port_aws( - security_group_name=security_group_name, - port_no=80, - cidr=aws_security_group_cidr, - region=region, - ) # HTTP - open_port_aws( - security_group_name=security_group_name, - port_no=443, - cidr=aws_security_group_cidr, - region=region, - ) # HTTPS - open_port_aws( - security_group_name=security_group_name, - port_no=22, - cidr=aws_security_group_cidr, - region=region, - ) # SSH - if kwargs["jupyter"]: - open_port_aws( - security_group_name=security_group_name, - port_no=8888, - cidr=aws_security_group_cidr, - region=region, - ) # Jupyter - - host_ips = make_aws_ec2_instance( - ami_id=ami_id, - ec2_instance_type=ec2_instance_type, - key_name=key_name, - security_group_name=security_group_name, - ) - - launch_cmds: list[str] = [] - - for host_ip in host_ips: - # get old host - host_term = verb.get_named_term_hostgrammar(name="host") - - # replace - host_term.parse_input(host_ip) - verb.set_named_term_type(name="host", new_term=host_term) - - if not bool(kwargs["provision"]): - print("Skipping automatic provisioning.") - print("VM created with:") - print(f"IP: {host_ip}") - print(f"Key: {key_path}") - print("\nConnect with:") - print(f"ssh -i {key_path} {username}@{host_ip}") - - else: - extra_kwargs = { - "repo": repo, - "branch": branch, - "ansible_extras": ansible_extras, - } - kwargs.update(extra_kwargs) - - # provision - host_up = check_ip_for_ssh(host_ip=host_ip) - if not host_up: - print(f"Warning: {host_ip} ssh not available yet") - launch_cmd = create_launch_custom_cmd(verb=verb, auth=auth, kwargs=kwargs) - launch_cmds.append(launch_cmd) - - return launch_cmds - - -def make_vm_azure( - node_name: str, - resource_group: str, - username: str, - password: str | None, - key_path: str | None, - size: str, - image_name: str, - node_count: int, -) -> list: - disk_size_gb = "200" - try: - temp_dir = tempfile.TemporaryDirectory() - public_key_path = ( - private_to_public_key( - private_key_path=key_path, temp_path=temp_dir.name, username=username - ) - if key_path - else None - ) - except Exception: # nosec - temp_dir.cleanup() - - authentication_type = "ssh" if key_path else "password" - cmd = f"az vm create -n {node_name} -g {resource_group} --size {size} " - cmd += f"--image {image_name} --os-disk-size-gb {disk_size_gb} " - cmd += f"--public-ip-sku Standard --authentication-type {authentication_type} --admin-username {username} " - cmd += f"--ssh-key-values {public_key_path} " if public_key_path else "" - cmd += f"--admin-password '{password}' " if password else "" - cmd += f"--count {node_count} " if node_count > 1 else "" - - host_ips: list | None = [] - try: - print(f"Creating vm.\nRunning: {hide_azure_vm_password(cmd)}") - subprocess.check_output(cmd, shell=True) # nosec - host_ips = get_vm_host_ips(node_name=node_name, resource_group=resource_group) - except Exception as e: - print("failed", e) - finally: - temp_dir.cleanup() - - if not host_ips: - raise Exception("Failed to create vm or get VM public ip") - - try: - # clean up temp public key - if public_key_path: - os.unlink(public_key_path) - except Exception: # nosec - pass - - return host_ips - - -def open_port_vm_azure( - resource_group: str, node_name: str, port_name: str, port: int, priority: int -) -> None: - cmd = f"az network nsg rule create --resource-group {resource_group} " - cmd += f"--nsg-name {node_name}NSG --name {port_name} --destination-port-ranges {port} --priority {priority}" - try: - print(f"Creating {port_name} {port} ngs rule.\nRunning: {cmd}") - output = subprocess.check_call(cmd, shell=True) # nosec - print("output", output) - pass - except Exception as e: - print("failed", e) - - -def create_project(project_id: str) -> None: - cmd = f"gcloud projects create {project_id} --set-as-default" - try: - print(f"Creating project.\nRunning: {cmd}") - subprocess.check_call(cmd, shell=True) # nosec - except Exception as e: - print("failed", e) - - print("create project complete") - - -def create_launch_gcp_cmd( - verb: GrammarVerb, - project_id: str, - zone: str, - machine_type: str, - ansible_extras: str, - kwargs: dict[str, Any], - repo: str, - branch: str, - auth: AuthCredentials, -) -> str: - # create project if it doesn't exist - create_project(project_id) - # vm - node_name = verb.get_named_term_type(name="node_name") - kebab_name = str(node_name.kebab_input) - disk_size_gb = "200" - host_ip = make_gcp_vm( - vm_name=kebab_name, - project_id=project_id, - zone=zone, - machine_type=machine_type, - disk_size_gb=disk_size_gb, - ) - - # get old host - host_term = verb.get_named_term_hostgrammar(name="host") - - host_up = check_ip_for_ssh(host_ip=host_ip) - if not host_up: - raise Exception(f"Something went wrong launching the VM at IP: {host_ip}.") - - if not bool(kwargs["provision"]): - print("Skipping automatic provisioning.") - print("VM created with:") - print(f"IP: {host_ip}") - print(f"User: {auth.username}") - print(f"Key: {auth.key_path}") - print("\nConnect with:") - print(f"ssh -i {auth.key_path} {auth.username}@{host_ip}") - sys.exit(0) - - # replace - host_term.parse_input(host_ip) - verb.set_named_term_type(name="host", new_term=host_term) - - extra_kwargs = { - "repo": repo, - "branch": branch, - "auth_type": "key", - "ansible_extras": ansible_extras, - } - kwargs.update(extra_kwargs) - - # provision - return create_launch_custom_cmd(verb=verb, auth=auth, kwargs=kwargs) - - -def make_gcp_vm( - vm_name: str, project_id: str, zone: str, machine_type: str, disk_size_gb: str -) -> str: - create_cmd = "gcloud compute instances create" - network_settings = "network=default,network-tier=PREMIUM" - maintenance_policy = "MIGRATE" - scopes = [ - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/logging.write", - "https://www.googleapis.com/auth/monitoring.write", - "https://www.googleapis.com/auth/servicecontrol", - "https://www.googleapis.com/auth/service.management.readonly", - "https://www.googleapis.com/auth/trace.append", - ] - tags = "http-server,https-server" - disk_image = "projects/ubuntu-os-cloud/global/images/ubuntu-2204-jammy-v20230429" - disk = ( - f"auto-delete=yes,boot=yes,device-name={vm_name},image={disk_image}," - + f"mode=rw,size={disk_size_gb},type=pd-ssd" - ) - security_flags = ( - "--no-shielded-secure-boot --shielded-vtpm " - + "--shielded-integrity-monitoring --reservation-affinity=any" - ) - - cmd = ( - f"{create_cmd} {vm_name} " - + f"--project={project_id} " - + f"--zone={zone} " - + f"--machine-type={machine_type} " - + f"--create-disk={disk} " - + f"--network-interface={network_settings} " - + f"--maintenance-policy={maintenance_policy} " - + f"--scopes={','.join(scopes)} --tags={tags} " - + f"{security_flags}" - ) - - host_ip = None - try: - print(f"Creating vm.\nRunning: {cmd}") - output = subprocess.check_output(cmd, shell=True) # nosec - host_ip = extract_host_ip_gcp(stdout=output) - except Exception as e: - print("failed", e) - - if host_ip is None: - raise Exception("Failed to create vm or get VM public ip") - - return host_ip - - -def create_launch_azure_cmd( - verb: GrammarVerb, - resource_group: str, - location: str, - size: str, - username: str, - password: str | None, - key_path: str | None, - repo: str, - branch: str, - auth: AuthCredentials, - ansible_extras: str, - kwargs: dict[str, Any], -) -> list[str]: - get_or_make_resource_group(resource_group=resource_group, location=location) - - node_count = kwargs.get("node_count", 1) - print("Total VMs to create: ", node_count) - - # vm - node_name = verb.get_named_term_type(name="node_name") - snake_name = str(node_name.snake_input) - image_name = get_azure_image(kwargs["image_name"]) - host_ips = make_vm_azure( - snake_name, - resource_group, - username, - password, - key_path, - size, - image_name, - node_count, - ) - - # open port 80 - open_port_vm_azure( - resource_group=resource_group, - node_name=snake_name, - port_name="HTTP", - port=80, - priority=500, - ) - - # open port 443 - open_port_vm_azure( - resource_group=resource_group, - node_name=snake_name, - port_name="HTTPS", - port=443, - priority=501, - ) - - if kwargs["jupyter"]: - # open port 8888 - open_port_vm_azure( - resource_group=resource_group, - node_name=snake_name, - port_name="Jupyter", - port=8888, - priority=502, - ) - - launch_cmds: list[str] = [] - - for host_ip in host_ips: - # get old host - host_term = verb.get_named_term_hostgrammar(name="host") - - # replace - host_term.parse_input(host_ip) - verb.set_named_term_type(name="host", new_term=host_term) - - if not bool(kwargs["provision"]): - print("Skipping automatic provisioning.") - print("VM created with:") - print(f"Name: {snake_name}") - print(f"IP: {host_ip}") - print(f"User: {username}") - print(f"Password: {password}") - print(f"Key: {key_path}") - print("\nConnect with:") - if kwargs["auth_type"] == "key": - print(f"ssh -i {key_path} {username}@{host_ip}") - else: - print(f"ssh {username}@{host_ip}") - else: - extra_kwargs = { - "repo": repo, - "branch": branch, - "ansible_extras": ansible_extras, - } - kwargs.update(extra_kwargs) - - # provision - host_up = check_ip_for_ssh(host_ip=host_ip) - if not host_up: - print(f"Warning: {host_ip} ssh not available yet") - launch_cmd = create_launch_custom_cmd(verb=verb, auth=auth, kwargs=kwargs) - launch_cmds.append(launch_cmd) - - return launch_cmds - - -def create_ansible_land_cmd( - verb: GrammarVerb, auth: AuthCredentials | None, kwargs: dict[str, Any] -) -> str: - try: - host_term = verb.get_named_term_hostgrammar(name="host") - print("Landing PyGrid node on port " + str(host_term.port) + "!\n") - - print(" - PORT: " + str(host_term.port)) - print("\n") - - grid_path = GRID_SRC_PATH() - playbook_path = grid_path + "/ansible/site.yml" - ansible_cfg_path = grid_path + "/ansible.cfg" - auth = cast(AuthCredentials, auth) - - if not os.path.exists(playbook_path): - print(f"Can't find playbook site.yml at: {playbook_path}") - cmd = f"ANSIBLE_CONFIG={ansible_cfg_path} ansible-playbook " - if host_term.host == "localhost": - cmd += "--connection=local " - cmd += f"-i {host_term.host}, {playbook_path}" - if host_term.host != "localhost" and kwargs["auth_type"] == "key": - cmd += f" --private-key {auth.key_path} --user {auth.username}" - elif host_term.host != "localhost" and kwargs["auth_type"] == "password": - cmd += f" -c paramiko --user {auth.username}" - - ANSIBLE_ARGS = {"install": "false"} - - if host_term.host != "localhost" and kwargs["auth_type"] == "password": - ANSIBLE_ARGS["ansible_ssh_pass"] = kwargs["password"] - - if host_term.host == "localhost": - ANSIBLE_ARGS["local"] = "true" - - if "ansible_extras" in kwargs and kwargs["ansible_extras"] != "": - options = kwargs["ansible_extras"].split(",") - for option in options: - parts = option.strip().split("=") - if len(parts) == 2: - ANSIBLE_ARGS[parts[0]] = parts[1] - - for k, v in ANSIBLE_ARGS.items(): - cmd += f" -e \"{k}='{v}'\"" - - cmd = "cd " + grid_path + ";" + cmd - return cmd - except Exception as e: - print(f"Failed to construct custom deployment cmd: {cmd}. {e}") - raise e - - -def create_launch_custom_cmd( - verb: GrammarVerb, auth: AuthCredentials | None, kwargs: dict[str, Any] -) -> str: - try: - host_term = verb.get_named_term_hostgrammar(name="host") - node_name = verb.get_named_term_type(name="node_name") - node_type = verb.get_named_term_type(name="node_type") - # source_term = verb.get_named_term_type(name="source") - - snake_name = str(node_name.snake_input) - - if ART: - hagrid() - - print( - "Launching a " - + str(node_type.input) - + " PyGrid node on port " - + str(host_term.port) - + "!\n" - ) - - print(" - TYPE: " + str(node_type.input)) - print(" - NAME: " + str(snake_name)) - print(" - PORT: " + str(host_term.port)) - print("\n") - - grid_path = GRID_SRC_PATH() - playbook_path = grid_path + "/ansible/site.yml" - ansible_cfg_path = grid_path + "/ansible.cfg" - auth = cast(AuthCredentials, auth) - - if not os.path.exists(playbook_path): - print(f"Can't find playbook site.yml at: {playbook_path}") - cmd = f"ANSIBLE_CONFIG={ansible_cfg_path} ansible-playbook " - if host_term.host == "localhost": - cmd += "--connection=local " - cmd += f"-i {host_term.host}, {playbook_path}" - if host_term.host != "localhost" and kwargs["auth_type"] == "key": - cmd += f" --private-key {auth.key_path} --user {auth.username}" - elif host_term.host != "localhost" and kwargs["auth_type"] == "password": - cmd += f" -c paramiko --user {auth.username}" - - version_string = kwargs["tag"] - if version_string is None: - version_string = "local" - - ANSIBLE_ARGS = { - "node_type": node_type.input, - "node_name": snake_name, - "github_repo": kwargs["repo"], - "repo_branch": kwargs["branch"], - "docker_tag": version_string, - } - - if host_term.host != "localhost" and kwargs["auth_type"] == "password": - ANSIBLE_ARGS["ansible_ssh_pass"] = kwargs["password"] - - if host_term.host == "localhost": - ANSIBLE_ARGS["local"] = "true" - - if "node_side_type" in kwargs: - ANSIBLE_ARGS["node_side_type"] = kwargs["node_side_type"] - - if kwargs["tls"] is True: - ANSIBLE_ARGS["tls"] = "true" - - if "release" in kwargs: - ANSIBLE_ARGS["release"] = kwargs["release"] - - if "set_root_email" in kwargs and kwargs["set_root_email"] is not None: - ANSIBLE_ARGS["root_user_email"] = kwargs["set_root_email"] - - if "set_root_password" in kwargs and kwargs["set_root_password"] is not None: - ANSIBLE_ARGS["root_user_password"] = kwargs["set_root_password"] - - if ( - kwargs["tls"] is True - and "cert_store_path" in kwargs - and len(kwargs["cert_store_path"]) > 0 - ): - ANSIBLE_ARGS["cert_store_path"] = kwargs["cert_store_path"] - - if ( - kwargs["tls"] is True - and "upload_tls_key" in kwargs - and len(kwargs["upload_tls_key"]) > 0 - ): - ANSIBLE_ARGS["upload_tls_key"] = kwargs["upload_tls_key"] - - if ( - kwargs["tls"] is True - and "upload_tls_cert" in kwargs - and len(kwargs["upload_tls_cert"]) > 0 - ): - ANSIBLE_ARGS["upload_tls_cert"] = kwargs["upload_tls_cert"] - - if kwargs["jupyter"] is True: - ANSIBLE_ARGS["jupyter"] = "true" - ANSIBLE_ARGS["jupyter_token"] = generate_sec_random_password( - length=48, upper_case=False, special_chars=False - ) - - if "ansible_extras" in kwargs and kwargs["ansible_extras"] != "": - options = kwargs["ansible_extras"].split(",") - for option in options: - parts = option.strip().split("=") - if len(parts) == 2: - ANSIBLE_ARGS[parts[0]] = parts[1] - - # if mode == "deploy": - # ANSIBLE_ARGS["deploy"] = "true" - - for k, v in ANSIBLE_ARGS.items(): - cmd += f" -e \"{k}='{v}'\"" - - cmd = "cd " + grid_path + ";" + cmd - return cmd - except Exception as e: - print(f"Failed to construct custom deployment cmd: {cmd}. {e}") - raise e - - -def create_land_cmd(verb: GrammarVerb, kwargs: dict[str, Any]) -> str: - host_term = verb.get_named_term_hostgrammar(name="host") - host = host_term.host if host_term.host is not None else "" - - if host in ["docker"]: - target = verb.get_named_term_grammar("node_name").input - prune_volumes: bool = kwargs.get("prune_vol", False) - - if target == "all": - # land all syft nodes - if prune_volumes: - land_cmd = "docker rm `docker ps --filter label=orgs.openmined.syft -q` --force " - land_cmd += "&& docker volume rm " - land_cmd += "$(docker volume ls --filter label=orgs.openmined.syft -q)" - return land_cmd - else: - return "docker rm `docker ps --filter label=orgs.openmined.syft -q` --force" - - version = check_docker_version() - if version: - return create_land_docker_cmd(verb=verb, prune_volumes=prune_volumes) - - elif host == "localhost" or is_valid_ip(host): - parsed_kwargs = {} - if DEPENDENCIES["ansible-playbook"]: - if host != "localhost": - parsed_kwargs["username"] = ask( - question=Question( - var_name="username", - question=f"Username for {host} with sudo privledges?", - default=arg_cache["username"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - parsed_kwargs["auth_type"] = ask( - question=Question( - var_name="auth_type", - question="Do you want to login with a key or password", - default=arg_cache["auth_type"], - kind="option", - options=["key", "password"], - cache=True, - ), - kwargs=kwargs, - ) - if parsed_kwargs["auth_type"] == "key": - parsed_kwargs["key_path"] = ask( - question=Question( - var_name="key_path", - question=f"Private key to access {parsed_kwargs['username']}@{host}?", - default=arg_cache["key_path"], - kind="path", - cache=True, - ), - kwargs=kwargs, - ) - elif parsed_kwargs["auth_type"] == "password": - parsed_kwargs["password"] = ask( - question=Question( - var_name="password", - question=f"Password for {parsed_kwargs['username']}@{host}?", - kind="password", - ), - kwargs=kwargs, - ) - - auth = None - if host != "localhost": - if parsed_kwargs["auth_type"] == "key": - auth = AuthCredentials( - username=parsed_kwargs["username"], - key_path=parsed_kwargs["key_path"], - ) - else: - auth = AuthCredentials( - username=parsed_kwargs["username"], - key_path=parsed_kwargs["password"], - ) - if not auth.valid: - raise Exception(f"Login Credentials are not valid. {auth}") - parsed_kwargs["ansible_extras"] = kwargs["ansible_extras"] - return create_ansible_land_cmd(verb=verb, auth=auth, kwargs=parsed_kwargs) - else: - errors = [] - if not DEPENDENCIES["ansible-playbook"]: - errors.append("ansible-playbook") - raise MissingDependency( - f"Launching a Custom VM requires: {' '.join(errors)}" - ) - - host_options = ", ".join(allowed_hosts) - raise MissingDependency( - f"Launch requires a correct host option, try: {host_options}" - ) - - -def create_land_docker_cmd(verb: GrammarVerb, prune_volumes: bool = False) -> str: - """ - Create docker `land` command to remove containers when a node's name is specified - """ - node_name = verb.get_named_term_type(name="node_name") - snake_name = str(node_name.snake_input) - - path = GRID_SRC_PATH() - env_var = ";export $(cat .env | sed 's/#.*//g' | xargs);" - - cmd = "" - cmd += "docker compose" - cmd += ' --file "docker-compose.yml"' - cmd += ' --project-name "' + snake_name + '"' - cmd += " down --remove-orphans" - - if prune_volumes: - cmd += ( - f' && docker volume rm $(docker volume ls --filter name="{snake_name}" -q)' - ) - - cmd += f" && docker rm $(docker ps --filter name={snake_name} -q) --force" - - cmd = "cd " + path + env_var + cmd - return cmd - - -@click.command( - help="Stop a running PyGrid domain/network node.", - context_settings={"show_default": True}, -) -@click.argument("args", type=str, nargs=-1) -@click.option( - "--cmd", - is_flag=True, - help="Print the cmd without running it", -) -@click.option( - "--ansible-extras", - default="", - type=str, -) -@click.option( - "--build-src", - default=DEFAULT_BRANCH, - required=False, - type=str, - help="Git branch to use for launch / build operations", -) -@click.option( - "--silent", - is_flag=True, - help="Suppress extra outputs", -) -@click.option( - "--force", - is_flag=True, - help="Bypass the prompt during hagrid land", -) -@click.option( - "--prune-vol", - is_flag=True, - help="Prune docker volumes after land.", -) -def land(args: tuple[str], **kwargs: Any) -> None: - verb = get_land_verb() - silent = bool(kwargs["silent"]) - force = bool(kwargs["force"]) - try: - grammar = parse_grammar(args=args, verb=verb) - verb.load_grammar(grammar=grammar) - except BadGrammar as e: - print(e) - return - - try: - update_repo(repo=GIT_REPO(), branch=str(kwargs["build_src"])) - except Exception as e: - print(f"Failed to update repo. {e}") - - try: - cmd = create_land_cmd(verb=verb, kwargs=kwargs) - except Exception as e: - print(f"{e}") - return - - target = verb.get_named_term_grammar("node_name").input - - if not force: - _land_domain = ask( - Question( - var_name="_land_domain", - question=f"Are you sure you want to land {target} (y/n)", - kind="yesno", - ), - kwargs={}, - ) - - grid_path = GRID_SRC_PATH() - - if force or _land_domain == "y": - if not bool(kwargs["cmd"]): - if not silent: - print("Running: \n", cmd) - try: - if silent: - process = subprocess.Popen( # nosec - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=grid_path, - shell=True, - ) - process.communicate() - - print(f"HAGrid land {target} complete!") - else: - subprocess.call(cmd, shell=True, cwd=grid_path) # nosec - except Exception as e: - print(f"Failed to run cmd: {cmd}. {e}") - else: - print("Hagrid land aborted.") - - -cli.add_command(launch) -cli.add_command(land) -cli.add_command(clean) - - -@click.command( - help="Show HAGrid debug information", context_settings={"show_default": True} -) -@click.argument("args", type=str, nargs=-1) -def debug(args: tuple[str], **kwargs: Any) -> None: - debug_info = gather_debug() - print("\n\nWhen reporting bugs, please copy everything between the lines.") - print("==================================================================\n") - print(json.dumps(debug_info)) - print("\n=================================================================\n\n") - - -cli.add_command(debug) - - -DEFAULT_HEALTH_CHECKS = ["host", "UI (Ξ²eta)", "api", "ssh", "jupyter"] -HEALTH_CHECK_FUNCTIONS = { - "host": check_host, - "UI (Ξ²eta)": check_login_page, - "api": check_api_metadata, - "ssh": check_ip_for_ssh, - "jupyter": check_jupyter_server, -} - -HEALTH_CHECK_ICONS = { - "host": "πŸ”Œ", - "UI (Ξ²eta)": "πŸ–±", - "api": "βš™οΈ", - "ssh": "πŸ”", - "jupyter": "πŸ“—", -} - -HEALTH_CHECK_URLS = { - "host": "{ip_address}", - "UI (Ξ²eta)": "http://{ip_address}/login", - "api": "http://{ip_address}/api/v2/openapi.json", - "ssh": "hagrid ssh {ip_address}", - "jupyter": "http://{ip_address}:8888", -} - - -def check_host_health(ip_address: str, keys: list[str]) -> dict[str, bool]: - status = {} - for key in keys: - func: Callable = HEALTH_CHECK_FUNCTIONS[key] # type: ignore - status[key] = func(ip_address, silent=True) - return status - - -def icon_status(status: bool) -> str: - return "βœ…" if status else "❌" - - -def get_health_checks(ip_address: str) -> tuple[bool, list[list[str]]]: - keys = list(DEFAULT_HEALTH_CHECKS) - if "localhost" in ip_address: - new_keys = [] - for key in keys: - if key not in ["host", "jupyter", "ssh"]: - new_keys.append(key) - keys = new_keys - - health_status = check_host_health(ip_address=ip_address, keys=keys) - complete_status = all(health_status.values()) - - # find port from ip_address - try: - port = int(ip_address.split(":")[1]) - except Exception: - # default to 80 - port = 80 - - # url to display based on running environment - display_url = gitpod_url(port).split("//")[1] if is_gitpod() else ip_address - - # figure out how to add this back? - # console.print("[bold magenta]Checking host:[/bold magenta]", ip_address, ":mage:") - table_contents = [] - for key, value in health_status.items(): - table_contents.append( - [ - HEALTH_CHECK_ICONS[key], - key, - HEALTH_CHECK_URLS[key].replace("{ip_address}", display_url), - icon_status(value), - ] - ) - - return complete_status, table_contents - - -def create_check_table( - table_contents: list[list[str]], time_left: int = 0 -) -> rich.table.Table: - table = rich.table.Table() - table.add_column("PyGrid", style="magenta") - table.add_column("Info", justify="left", overflow="fold") - time_left_str = "" if time_left == 0 else str(time_left) - table.add_column(time_left_str, justify="left") - for row in table_contents: - table.add_row(row[1], row[2], row[3]) - return table - - -def get_host_name(container_name: str, by_suffix: str) -> str: - # Assumption we always get proxy containers first. - # if users have old docker compose versios. - # the container names are _ instead of - - # canada_proxy_1 instead of canada-proxy-1 - try: - host_name = container_name[0 : container_name.find(by_suffix) - 1] # noqa: E203 - except Exception: - host_name = "" - return host_name - - -def get_docker_status( - ip_address: str, node_name: str | None -) -> tuple[bool, tuple[str, str]]: - url = from_url(ip_address) - port = url[2] - network_container = ( - shell( - "docker ps --format '{{.Names}} {{.Ports}}' | " + f"grep '0.0.0.0:{port}'" - ) - .strip() - .split(" ")[0] - ) - - # Second conditional handle the case when internal port of worker container - # matches with host port of launched Domain/Network Container - if not network_container or (node_name and node_name not in network_container): - # check if it is a worker container and an internal port was passed - worker_containers_output: str = shell( - "docker ps --format '{{.Names}} {{.Ports}}' | " + f"grep '{port}/tcp'" - ).strip() - if not worker_containers_output or not node_name: - return False, ("", "") - - # If there are worker containers with an internal port - # fetch the worker container with the launched worker name - worker_containers = worker_containers_output.split("\n") - for worker_container in worker_containers: - container_name = worker_container.split(" ")[0] - if node_name in container_name: - network_container = container_name - break - else: - # If the worker container is not created yet - return False, ("", "") - - if "proxy" in network_container: - host_name = get_host_name(network_container, by_suffix="proxy") - - backend_containers = shell( - "docker ps --format '{{.Names}}' | grep 'backend' " - ).split() - - _backend_exists = False - for container in backend_containers: - if host_name in container and "stream" not in container: - _backend_exists = True - break - if not _backend_exists: - return False, ("", "") - - node_type = "Domain" - - # TODO: Identify if node_type is Gateway - # for container in headscale_containers: - # if host_name in container: - # node_type = "Gateway" - # break - - return True, (host_name, node_type) - else: - # health check for worker node - host_name = get_host_name(network_container, by_suffix="worker") - return True, (host_name, "Worker") - - -def get_syft_install_status(host_name: str, node_type: str) -> bool: - container_search = "backend" if node_type != "Worker" else "worker" - search_containers = shell( - "docker ps --format '{{.Names}}' | " + f"grep '{container_search}' " - ).split() - - context_container = None - for container in search_containers: - # stream keyword is for our old container stack - if host_name in container and "stream" not in container: - context_container = container - break - - if not context_container: - print(f"❌ {container_search} Docker Stack for: {host_name} not found") - exit(0) - else: - container_log = shell(f"docker logs {context_container}") - if "Application startup complete" not in container_log: - return False - return True - - -@click.command( - help="Check health of an IP address/addresses or a resource group", - context_settings={"show_default": True}, -) -@click.argument("ip_addresses", type=str, nargs=-1) -@click.option( - "--timeout", - default=300, - help="Timeout for hagrid check command", -) -@click.option( - "--verbose", - is_flag=True, - help="Refresh output", -) -def check( - ip_addresses: list[str], verbose: bool = False, timeout: int | str = 300 -) -> None: - check_status(ip_addresses=ip_addresses, silent=not verbose, timeout=timeout) - - -def _check_status( - ip_addresses: str | list[str], - silent: bool = True, - signal: Event | None = None, - node_name: str | None = None, -) -> None: - OK_EMOJI = RichEmoji("white_heavy_check_mark").to_str() - # Check if ip_addresses is str, then convert to list - if ip_addresses and isinstance(ip_addresses, str): - ip_addresses = [ip_addresses] - console = Console() - node_info = None - if len(ip_addresses) == 0: - headers = {"User-Agent": "curl/7.79.1"} - print("Detecting External IP...") - ip_res = requests.get("https://ifconfig.co", headers=headers) # nosec - ip_address = ip_res.text.strip() - ip_addresses = [ip_address] - - if len(ip_addresses) == 1: - ip_address = ip_addresses[0] - status, table_contents = get_health_checks(ip_address=ip_address) - table = create_check_table(table_contents=table_contents) - max_timeout = 600 - if not status: - table = create_check_table( - table_contents=table_contents, time_left=max_timeout - ) - if silent: - with console.status("Gathering Node information") as console_status: - console_status.update( - "[bold orange_red1]Waiting for Container Creation" - ) - docker_status, node_info = get_docker_status(ip_address, node_name) - while not docker_status: - docker_status, node_info = get_docker_status( - ip_address, node_name - ) - time.sleep(1) - if ( - signal and signal.is_set() - ): # Stop execution if timeout is triggered - return - console.print( - f"{OK_EMOJI} {node_info[0]} {node_info[1]} Containers Created" - ) - - console_status.update("[bold orange_red1]Starting Backend") - syft_install_status = get_syft_install_status( - node_info[0], node_info[1] - ) - while not syft_install_status: - syft_install_status = get_syft_install_status( - node_info[0], node_info[1] - ) - time.sleep(1) - # Stop execution if timeout is triggered - if signal and signal.is_set(): - return - console.print(f"{OK_EMOJI} Backend") - console.print(f"{OK_EMOJI} Startup Complete") - - status, table_contents = get_health_checks(ip_address) - table = create_check_table( - table_contents=table_contents, time_left=max_timeout - ) - else: - while not status: - # Stop execution if timeout is triggered - if signal is not None and signal.is_set(): - return - with Live( - table, refresh_per_second=2, screen=True, auto_refresh=False - ) as live: - max_timeout -= 1 - if max_timeout % 5 == 0: - status, table_contents = get_health_checks(ip_address) - table = create_check_table( - table_contents=table_contents, time_left=max_timeout - ) - live.update(table) - if status: - break - time.sleep(1) - - # TODO: Create new health checks table for Worker Container - if (node_info and node_info[1] != "Worker") or not node_info: - console.print(table) - else: - for ip_address in ip_addresses: - _, table_contents = get_health_checks(ip_address) - table = create_check_table(table_contents=table_contents) - console.print(table) - - -def check_status( - ip_addresses: str | list[str], - silent: bool = True, - timeout: int | str = 300, - node_name: str | None = None, -) -> None: - timeout = int(timeout) - # third party - from rich import print - - signal = Event() - - t = Thread( - target=_check_status, - kwargs={ - "ip_addresses": ip_addresses, - "silent": silent, - "signal": signal, - "node_name": node_name, - }, - ) - t.start() - t.join(timeout=timeout) - - if t.is_alive(): - signal.set() - t.join() - - print(f"Hagrid check command timed out after: {timeout} seconds πŸ•›") - print( - "Please try increasing the timeout or kindly check the docker containers for error logs." - ) - print("You can view your container logs using the following tool:") - print("Tool: [link=https://ctop.sh]Ctop[/link]") - print("Video Explanation: https://youtu.be/BJhlCxerQP4 \n") - - -cli.add_command(check) - - -# add Hagrid info to the cli -@click.command(help="Show HAGrid info", context_settings={"show_default": True}) -def version() -> None: - print(f"HAGRID_VERSION: {get_version_string()}") - if EDITABLE_MODE: - print(f"HAGRID_REPO_SHA: {commit_hash()}") - - -cli.add_command(version) - - -def run_quickstart( - url: str | None = None, - syft: str = "latest", - reset: bool = False, - quiet: bool = False, - pre: bool = False, - test: bool = False, - repo: str = DEFAULT_REPO, - branch: str = DEFAULT_BRANCH, - commit: str | None = None, - python: str | None = None, - zip_file: str | None = None, -) -> None: - try: - quickstart_art() - directory = os.path.expanduser("~/.hagrid/quickstart/") - confirm_reset = None - if reset: - if not quiet: - confirm_reset = click.confirm( - "This will create a new quickstart virtualenv and reinstall Syft and " - "Jupyter. Are you sure you want to continue?" - ) - else: - confirm_reset = True - if confirm_reset is False: - return - - if reset and confirm_reset or not os.path.isdir(directory): - quickstart_setup( - directory=directory, - syft_version=syft, - reset=reset, - pre=pre, - python=python, - ) - downloaded_files = [] - if zip_file: - downloaded_files = fetch_notebooks_from_zipfile( - zip_file, - directory=directory, - reset=reset, - ) - elif url: - downloaded_files = fetch_notebooks_for_url( - url=url, - directory=directory, - reset=reset, - repo=repo, - branch=branch, - commit=commit, - ) - else: - file_path = add_intro_notebook(directory=directory, reset=reset) - downloaded_files.append(file_path) - - if len(downloaded_files) == 0: - raise Exception(f"Unable to find files at: {url}") - file_path = sorted(downloaded_files)[0] - - # add virtualenv path - environ = os.environ.copy() - os_bin_path = "Scripts" if is_windows() else "bin" - venv_dir = directory + ".venv" - environ["PATH"] = venv_dir + os.sep + os_bin_path + os.pathsep + environ["PATH"] - jupyter_binary = "jupyter.exe" if is_windows() else "jupyter" - - if is_windows(): - env_activate_cmd = ( - "(Powershell): " - + "cd " - + venv_dir - + "; " - + os_bin_path - + os.sep - + "activate" - ) - else: - env_activate_cmd = ( - "(Linux): source " + venv_dir + os.sep + os_bin_path + "/activate" - ) - - print(f"To activate your virtualenv {env_activate_cmd}") - - try: - allow_browser = " --no-browser" if is_gitpod() else "" - cmd = ( - venv_dir - + os.sep - + os_bin_path - + os.sep - + f"{jupyter_binary} lab{allow_browser} --ip 0.0.0.0 --notebook-dir={directory} {file_path}" - ) - if test: - jupyter_path = venv_dir + os.sep + os_bin_path + os.sep + jupyter_binary - if not os.path.exists(jupyter_path): - print(f"Failed to install Jupyter in path: {jupyter_path}") - sys.exit(1) - print(f"Jupyter exists at: {jupyter_path}. CI Test mode exiting.") - sys.exit(0) - - disable_toolbar_extension = ( - venv_dir - + os.sep - + os_bin_path - + os.sep - + f"{jupyter_binary} labextension disable @jupyterlab/cell-toolbar-extension" - ) - - subprocess.run( # nosec - disable_toolbar_extension.split(" "), cwd=directory, env=environ - ) - - ON_POSIX = "posix" in sys.builtin_module_names - - def enqueue_output(out: Any, queue: Queue) -> None: - for line in iter(out.readline, b""): - queue.put(line) - out.close() - - proc = subprocess.Popen( # nosec - cmd.split(" "), - cwd=directory, - env=environ, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=ON_POSIX, - ) - queue: Queue = Queue() - thread_1 = Thread(target=enqueue_output, args=(proc.stdout, queue)) - thread_2 = Thread(target=enqueue_output, args=(proc.stderr, queue)) - thread_1.daemon = True # thread dies with the program - thread_1.start() - thread_2.daemon = True # thread dies with the program - thread_2.start() - - display_url = None - console = rich.get_console() - - # keepn reading the queue of stdout + stderr - while True: - try: - if not display_url: - # try to read the line and extract a jupyter url: - with console.status( - "Starting Jupyter service" - ) as console_status: - line = queue.get() - display_url = extract_jupyter_url(line.decode("utf-8")) - if display_url: - display_jupyter_url(url_parts=display_url) - console_status.stop() - except KeyboardInterrupt: - proc.kill() # make sure jupyter gets killed - sys.exit(1) - except Exception: # nosec - pass # nosec - except KeyboardInterrupt: - proc.kill() # make sure jupyter gets killed - sys.exit(1) - except Exception as e: - print(f"Error running quickstart: {e}") - raise e - - -@click.command( - help="Launch a Syft + Jupyter Session with a Notebook URL / Path", - context_settings={"show_default": True}, -) -@click.argument("url", type=str, required=False) -@click.option( - "--reset", - is_flag=True, - default=False, - help="Force hagrid quickstart to setup a fresh virtualenv", -) -@click.option( - "--syft", - default="latest", - help="Choose a syft version or just use latest", -) -@click.option( - "--quiet", - is_flag=True, - help="Silence confirmation prompts", -) -@click.option( - "--pre", - is_flag=True, - help="Install pre-release versions of syft", -) -@click.option( - "--python", - default=None, - help="Specify the path to which python to use", -) -@click.option( - "--test", - is_flag=True, - help="CI Test Mode, don't hang on Jupyter", -) -@click.option( - "--repo", - default=DEFAULT_REPO, - help="Choose a repo to fetch the notebook from or just use OpenMined/PySyft", -) -@click.option( - "--branch", - default=DEFAULT_BRANCH, - help="Choose a branch to fetch from or just use dev", -) -@click.option( - "--commit", - help="Choose a specific commit to fetch the notebook from", -) -def quickstart_cli( - url: str | None = None, - syft: str = "latest", - reset: bool = False, - quiet: bool = False, - pre: bool = False, - test: bool = False, - repo: str = DEFAULT_REPO, - branch: str = DEFAULT_BRANCH, - commit: str | None = None, - python: str | None = None, -) -> None: - return run_quickstart( - url=url, - syft=syft, - reset=reset, - quiet=quiet, - pre=pre, - test=test, - repo=repo, - branch=branch, - commit=commit, - python=python, - ) - - -cli.add_command(quickstart_cli, "quickstart") - - -def display_jupyter_url(url_parts: tuple[str, str, int]) -> None: - url = url_parts[0] - if is_gitpod(): - parts = urlparse(url) - query = getattr(parts, "query", "") - url = gitpod_url(port=url_parts[2]) + "?" + query - - console = rich.get_console() - - tick_emoji = RichEmoji("white_heavy_check_mark").to_str() - link_emoji = RichEmoji("link").to_str() - - console.print( - f"[bold white]{tick_emoji} Jupyter Server is running at:\n{link_emoji} [bold blue]{url}\n" - + "[bold white]Use Control-C to stop this server and shut down all kernels.", - new_line_start=True, - ) - - # if is_gitpod(): - # open_browser_with_url(url=url) - - -def open_browser_with_url(url: str) -> None: - webbrowser.open(url) - - -def extract_jupyter_url(line: str) -> tuple[str, str, int] | None: - jupyter_regex = r"^.*(http.*127.*)" - try: - matches = re.match(jupyter_regex, line) - if matches is not None: - url = matches.group(1).strip() - parts = urlparse(url) - host_or_ip_parts = parts.netloc.split(":") - # netloc is host:port - port = 8888 - if len(host_or_ip_parts) > 1: - port = int(host_or_ip_parts[1]) - host_or_ip = host_or_ip_parts[0] - return (url, host_or_ip, port) - except Exception as e: - print("failed to parse jupyter url", e) - return None - - -def quickstart_setup( - directory: str, - syft_version: str, - reset: bool = False, - pre: bool = False, - python: str | None = None, -) -> None: - console = rich.get_console() - OK_EMOJI = RichEmoji("white_heavy_check_mark").to_str() - - try: - with console.status( - "[bold blue]Setting up Quickstart Environment" - ) as console_status: - os.makedirs(directory, exist_ok=True) - virtual_env_dir = os.path.abspath(directory + ".venv/") - if reset and os.path.exists(virtual_env_dir): - shutil.rmtree(virtual_env_dir) - env = VirtualEnvironment(virtual_env_dir, python=python) - console.print( - f"{OK_EMOJI} Created Virtual Environment {RichEmoji('evergreen_tree').to_str()}" - ) - - # upgrade pip - console_status.update("[bold blue]Installing pip") - env.install("pip", options=["-U"]) - console.print(f"{OK_EMOJI} pip") - - # upgrade packaging - console_status.update("[bold blue]Installing packaging") - env.install("packaging", options=["-U"]) - console.print(f"{OK_EMOJI} packaging") - - # Install jupyter lab - console_status.update("[bold blue]Installing Jupyter Lab") - env.install("jupyterlab") - env.install("ipywidgets") - console.print(f"{OK_EMOJI} Jupyter Lab") - - # Install hagrid - if EDITABLE_MODE: - local_hagrid_dir = Path( - os.path.abspath(Path(hagrid_root()) / "../hagrid") - ) - console_status.update( - f"[bold blue]Installing HAGrid in Editable Mode: {str(local_hagrid_dir)}" - ) - env.install("-e " + str(local_hagrid_dir)) - console.print( - f"{OK_EMOJI} HAGrid in Editable Mode: {str(local_hagrid_dir)}" - ) - else: - console_status.update("[bold blue]Installing hagrid") - env.install("hagrid", options=["-U"]) - console.print(f"{OK_EMOJI} HAGrid") - except Exception as e: - print(e) - raise e - - -def add_intro_notebook(directory: str, reset: bool = False) -> str: - filenames = ["00-quickstart.ipynb", "01-install-wizard.ipynb"] - - files = os.listdir(directory) - try: - files.remove(".venv") - except Exception: # nosec - pass - - existing = 0 - for file in files: - if file in filenames: - existing += 1 - - if existing != len(filenames) or reset: - if EDITABLE_MODE: - local_src_dir = Path(os.path.abspath(Path(hagrid_root()) / "../../")) - for filename in filenames: - file_path = os.path.abspath(f"{directory}/{filename}") - shutil.copyfile( - local_src_dir / f"notebooks/quickstart/{filename}", - file_path, - ) - else: - for filename in filenames: - url = ( - "https://raw.githubusercontent.com/OpenMined/PySyft/dev/" - + f"notebooks/quickstart/{filename}" - ) - file_path, _, _ = quickstart_download_notebook( - url=url, directory=directory, reset=reset - ) - if arg_cache["install_wizard_complete"]: - filename = filenames[0] - else: - filename = filenames[1] - return os.path.abspath(f"{directory}/{filename}") - - -@click.command(help="Walk the Path", context_settings={"show_default": True}) -@click.argument("zip_file", type=str, default="padawan.zip", metavar="ZIPFILE") -def dagobah(zip_file: str) -> None: - if not os.path.exists(zip_file): - for text in ( - f"{zip_file} does not exists.", - "Please specify the path to the zip file containing the notebooks.", - "hagrid dagobah [ZIPFILE]", - ): - print(text, file=sys.stderr) - sys.exit(1) - - return run_quickstart(zip_file=zip_file) - - -cli.add_command(dagobah) - - -def ssh_into_remote_machine( - host_ip: str, - username: str, - auth_type: str, - private_key_path: str | None, - cmd: str = "", -) -> None: - """Access or execute command on the remote machine. - - Args: - host_ip (str): ip address of the VM - private_key_path (str): private key of the VM - username (str): username on the VM - cmd (str, optional): Command to execute on the remote machine. Defaults to "". - """ - try: - if auth_type == "key": - subprocess.call( # nosec - ["ssh", "-i", f"{private_key_path}", f"{username}@{host_ip}", cmd] - ) - elif auth_type == "password": - subprocess.call(["ssh", f"{username}@{host_ip}", cmd]) # nosec - except Exception as e: - raise e - - -@click.command( - help="SSH into the IP address or a resource group", - context_settings={"show_default": True}, -) -@click.argument("ip_address", type=str) -@click.option( - "--cmd", - type=str, - required=False, - default="", - help="Optional: command to execute on the remote machine.", -) -def ssh(ip_address: str, cmd: str) -> None: - kwargs: dict = {} - key_path: str | None = None - - if check_ip_for_ssh(ip_address, timeout=10, silent=False): - username = ask( - question=Question( - var_name="azure_username", - question="What is the username for the VM?", - default=arg_cache["azure_username"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - auth_type = ask( - question=Question( - var_name="auth_type", - question="Do you want to login with a key or password", - default=arg_cache["auth_type"], - kind="option", - options=["key", "password"], - cache=True, - ), - kwargs=kwargs, - ) - - if auth_type == "key": - key_path = ask( - question=Question( - var_name="azure_key_path", - question="Absolute path to the private key of the VM?", - default=arg_cache["azure_key_path"], - kind="string", - cache=True, - ), - kwargs=kwargs, - ) - - # SSH into the remote and execute the command - ssh_into_remote_machine( - host_ip=ip_address, - username=username, - auth_type=auth_type, - private_key_path=key_path, - cmd=cmd, - ) - - -cli.add_command(ssh) - - -# Add hagrid logs command to the CLI -@click.command( - help="Get the logs of the HAGrid node", context_settings={"show_default": True} -) -@click.argument("domain_name", type=str) -def logs(domain_name: str) -> None: # nosec - container_ids = ( - subprocess.check_output( # nosec - f"docker ps -qf name=^{domain_name}-*", shell=True - ) - .decode("utf-8") - .split() - ) - Container = namedtuple("Container", "id name logs") - container_names = [] - for container in container_ids: - container_name = ( - subprocess.check_output( # nosec - "docker inspect --format '{{.Name}}' " + container, shell=True - ) - .decode("utf-8") - .strip() - .replace("/", "") - ) - log_command = "docker logs -f " + container_name - container_names.append( - Container(id=container, name=container_name, logs=log_command) - ) - # Generate a table of the containers and their logs with Rich - table = rich.table.Table(title="Container Logs") - table.add_column("Container ID", justify="center", style="cyan", no_wrap=True) - table.add_column("Container Name", justify="right", style="cyan", no_wrap=True) - table.add_column("Log Command", justify="right", style="cyan", no_wrap=True) - for container in container_names: # type: ignore - table.add_row(container.id, container.name, container.logs) # type: ignore - console = rich.console.Console() - console.print(table) - # Print instructions on how to view the logs - console.print( - rich.panel.Panel( - long_string, - title="How to view logs", - border_style="white", - expand=False, - padding=1, - highlight=True, - ) - ) - - -long_string = ( - "β„Ή [bold green]To view the live logs of a container,copy the log command and paste it into your terminal.[/bold green]\n" # noqa: E501 - + "\n" - + "β„Ή [bold green]The logs will be streamed to your terminal until you exit the command.[/bold green]\n" - + "\n" - + "β„Ή [bold green]To exit the logs, press CTRL+C.[/bold green]\n" - + "\n" - + "🚨 The [bold white]backend,backend_stream & celery[/bold white] [bold green]containers are the most important to monitor for debugging.[/bold green]\n" # noqa: E501 - + "\n" - + " [bold white]--------------- Ctop 🦾 -------------------------[/bold white]\n" - + "\n" - + "🧠 To learn about using [bold white]ctop[/bold white] to monitor your containers,visit https://www.youtube.com/watch?v=BJhlCxerQP4n \n" # noqa: E501 - + "\n" - + " [bold white]----------------- How to view this. πŸ™‚ ---------------[/bold white]\n" - + "\n" - + """β„Ή [bold green]To view this panel again, run the command [bold white]hagrid logs {{NODE_NAME}}[/bold white] [/bold green]\n""" # noqa: E501 - + "\n" - + """🚨 NODE_NAME above is the name of your Hagrid deployment,without the curly braces. E.g hagrid logs canada [bold green]\n""" # noqa: E501 - + "\n" - + " [bold green]HAPPY DEBUGGING! πŸ›πŸžπŸ¦—πŸ¦ŸπŸ¦ πŸ¦ πŸ¦ [/bold green]\n " -) - -cli.add_command(logs) diff --git a/packages/hagrid/hagrid/deps.py b/packages/hagrid/hagrid/deps.py deleted file mode 100644 index 2a97353ce7c..00000000000 --- a/packages/hagrid/hagrid/deps.py +++ /dev/null @@ -1,911 +0,0 @@ -"""The purpose of these functions is to check the local dependencies of the person running the CLI -tool and ensure that things are properly configured for the cli's full use (depending on the user's -operating system.) When dependencies are missing the CLI tool should offer helpful hints about what -course of action to take to install missing dependencies, even offering to run appropriate -installation commands where applicable.""" - -# future -from __future__ import annotations - -# stdlib -from collections.abc import Callable -from dataclasses import dataclass -from dataclasses import field -from datetime import datetime -import getpass -import json -import os -import platform -import re -import shutil -import subprocess # nosec -import sys -import traceback -from typing import Any - -# third party -from packaging import version -from packaging.version import Version -import requests -from rich.console import Console - -# relative -from .exceptions import MissingDependency -from .lib import is_gitpod -from .mode import EDITABLE_MODE -from .nb_output import NBOutput -from .version import __version__ - -LATEST_BETA_SYFT = "0.8.7-beta.7" - -DOCKER_ERROR = """ -You are running an old version of docker, possibly on Linux. You need to install v2. -At the time of writing this, if you are on linux you need to run the following: - -DOCKER_COMPOSE_VERSION=v2.21.0 -curl -sSL https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 \ - -o ~/.docker/cli-plugins/docker-compose -chmod +x ~/.docker/cli-plugins/docker-compose - -ALERT: you may need to run the following command to make sure you can run without sudo. - -echo $USER //(should return your username) -sudo usermod -aG docker $USER - -... now LOG ALL THE WAY OUT!!! - -...and then you should be good to go. You can check your installation by running: - -docker compose version -""" - -SYFT_MINIMUM_PYTHON_VERSION = (3, 10) -SYFT_MINIMUM_PYTHON_VERSION_STRING = "3.10" -SYFT_MAXIMUM_PYTHON_VERSION = (3, 12, 999) -SYFT_MAXIMUM_PYTHON_VERSION_STRING = "3.12" -WHITE = "\033[0;37m" -GREEN = "\033[0;32m" -YELLOW = "\033[0;33m" -BOLD = "\033[1m" -NO_COLOR = "\033[0;0m" -WARNING_MSG = f"\033[0;33mWARNING:{NO_COLOR}" - - -def get_version_string() -> str: - version = str(__version__) - if EDITABLE_MODE: - version += "-dev" - return version - - -@dataclass -class SetupIssue: - issue_name: str - description: str - command: str | None = None - solution: str | None = None - - -@dataclass -class Dependency: - of: str = "" - name: str = "" - display: str = "" - only_os: str = "" - version: Version | None = version.parse("0.0") - valid: bool = False - issues: list[SetupIssue] = field(default_factory=list) - output_in_text: bool = False - - def check(self) -> None: - pass - - -@dataclass -class DependencySyftOS(Dependency): - of: str = "syft" - - def check(self) -> None: - self.display = "βœ… " + ENVIRONMENT["os"] - if is_windows(): - pass - elif is_apple_silicon(): - pass - - -@dataclass -class DependencySyftPython(Dependency): - of: str = "syft" - - def check(self) -> None: - self.version = sys.version_info - if ( - sys.version_info >= SYFT_MINIMUM_PYTHON_VERSION - and sys.version_info <= SYFT_MAXIMUM_PYTHON_VERSION - ): - self.display = "βœ… Python " + ENVIRONMENT["python_version"] - else: - self.issues.append(python_version_unsupported()) - self.display = "❌ " + ENVIRONMENT["python_version"] - - -@dataclass -class DependencyGridGit(Dependency): - of: str = "grid" - - def check(self) -> None: - binary_info = BinaryInfo( - binary="git", version_cmd="git --version" - ).get_binary_info() - if binary_info.path and binary_info.version: - self.display = "βœ… Git " + str(binary_info.version) - else: - self.issues.append(git_install(self.output_in_text)) - self.display = "❌ Git not installed" - - -MINIMUM_DOCKER_VERSION = "20.0.0" - - -@dataclass -class DependencyGridDocker(Dependency): - of: str = "grid" - - def check(self) -> None: - binary_info = BinaryInfo( - binary="docker", version_cmd="docker --version" - ).get_binary_info() - if binary_info.path and binary_info.version > version.parse( - MINIMUM_DOCKER_VERSION - ): - self.display = "βœ… Docker " + str(binary_info.version) - else: - self.issues.append(docker_install()) - self.display = "❌ Docker not installed" - - -MINIMUM_DOCKER_COMPOSE_VERSION = "2.0.0" - - -@dataclass -class DependencyGridDockerCompose(Dependency): - of: str = "grid" - - def check(self) -> None: - binary_info = BinaryInfo( - binary="docker", version_cmd="docker compose version" - ).get_binary_info() - - if ( - binary_info.path - and binary_info.version - and binary_info.version > version.parse(MINIMUM_DOCKER_COMPOSE_VERSION) - ): - self.display = "βœ… Docker Compose " + str(binary_info.version) - else: - self.issues.append(docker_compose_install()) - self.display = "❌ Docker Compose v2 not installed" - - -@dataclass -class DependencyPyPI(Dependency): - of: str = "none" - package_name: str = "" - package_display_name: str = "" - pre: bool = False - install_issue: Callable = lambda: None # noqa: E731 - update_available_issue: Callable = lambda: None # noqa: E731 - - def check(self) -> None: - package_dict = get_pip_package(self.package_name) - - if package_dict is None: - self.display = "❌ " + f"{self.package_display_name} not installed" - self.issues.append(self.install_issue(pre=self.pre)) - else: - version_string = package_dict["version"] - current_version = version.parse(version_string) - if "editable_project_location" in package_dict: - self.display = ( - "🚨 " - + f"{self.package_name}=={str(current_version)} -e {package_dict['editable_project_location']}" - ) - else: - is_newer, latest_version = new_pypi_version( - package=self.package_name, current=current_version, pre=self.pre - ) - if not is_newer: - channel = "stable" - if current_version.is_prerelease: - channel = "pre-release" - self.display = ( - "βœ… " - + f"{self.package_name}=={str(version_string)} (latest {channel})" - ) - else: - self.display = ( - "βœ… " - + f"{self.package_name}=={str(current_version)} (Version {str(latest_version)} available)" - ) - self.issues.append( - self.update_available_issue(current_version, latest_version) - ) - - -def new_pypi_version( - package: str, current: Version, pre: bool = False -) -> tuple[bool, Version]: - pypi_json = get_pypi_versions(package_name=package) - if ( - "info" not in pypi_json - or "releases" not in pypi_json - or "version" not in pypi_json["info"] - ): - raise Exception("Bad response from PyPi") - - if not current.is_prerelease and not pre: - latest_stable = version.parse(pypi_json["info"]["version"]) - if current < latest_stable: - return (True, latest_stable) - else: - return (False, current) - else: - latest_release = current - - releases = sorted(pypi_json["releases"].keys()) - for release in releases: - pre_release_version = version.parse(release) - if latest_release < pre_release_version: - latest_release = pre_release_version - - if latest_release != current: - return (True, latest_release) - else: - return (False, latest_release) - - -def get_pypi_versions(package_name: str) -> dict[str, Any]: - try: - pypi_url = f"https://pypi.org/pypi/{package_name}/json" - req = requests.get(pypi_url) # nosec - # TODO: Fix JSON parsing of version keys - # this is broken on my machine for some reason, the version keys are wrong - pypi_info = json.loads(req.text) - # print(pypi_info["releases"].keys()) - return pypi_info - - except Exception as e: - print(f"Unable to get JSON from PyPI URL: {pypi_url}. {e}") - raise e - - -def get_pip_package(package_name: str) -> dict[str, str] | None: - packages = get_pip_packages() - for package in packages: - if package["name"] == package_name: - return package - return None - - -def get_pip_packages() -> list[dict[str, str]]: - try: - cmd = "python -m pip list --format=json --disable-pip-version-check" - output = subprocess.check_output(cmd, shell=True) # nosec - return json.loads(str(output.decode("utf-8")).strip()) - except Exception as e: - print("failed to pip list", e) - raise e - - -def get_location(binary: str) -> str | None: - return shutil.which(binary) - - -@dataclass -class BinaryInfo: - binary: str - version_cmd: str - error: str | None = None - path: str | None = None - version: str | Version | None = version.parse("0.0") - version_regex = ( - r"[^\d]*(" - + r"(0|[1-9][0-9]*)\.*(0|[1-9][0-9]*)\.*(0|[1-9][0-9]*)" - + r"(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)" - + r"(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?" - + r"(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?)" - + r"[^\d].*" - ) - - def extract_version(self, lines: list[str]) -> None: - for line in lines: - matches = re.match(self.version_regex, line) - if matches is not None: - self.version = matches.group(1) - try: - if "-gitpod" in self.version: - parts = self.version.split("-gitpod") - self.version = parts[0] - if "-desktop" in self.version: - parts = self.version.split("-desktop") - self.version = parts[0] - self.version = version.parse(self.version) - except Exception: # nosec - pass - break - - def get_binary_info(self) -> BinaryInfo: - self.path = get_location(self.binary) - if self.path: - returncode, lines = get_cli_output(self.version_cmd) - if returncode == 0: - self.extract_version(lines=lines) - else: - if len(lines) > 0: - self.error = lines[0] - else: - self.error = f"Error, no output from {self.binary}" - return self - - -def get_cli_output(cmd: str, timeout: float | None = None) -> tuple[int, list[str]]: - try: - proc = subprocess.Popen( # nosec - cmd.split(" "), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - lines = [] - if proc.stdout and hasattr(proc.stdout, "readlines"): - lines = [line.decode("utf-8") for line in proc.stdout.readlines()] - - if proc.stderr and hasattr(proc.stderr, "readlines"): - lines.extend([line.decode("utf-8") for line in proc.stderr.readlines()]) - - proc.communicate(timeout=timeout) - return (int(proc.returncode), lines) - except Exception as e: - return (-1, [str(e)]) - - -def gather_debug() -> dict[str, Any]: - # relative - from .lib import commit_hash - from .lib import hagrid_root - - now = datetime.now().astimezone() - dt_string = now.strftime("%d/%m/%Y %H:%M:%S %Z") - debug_info: dict[str, Any] = {} - debug_info["datetime"] = dt_string - debug_info["python_binary"] = sys.executable - debug_info["dependencies"] = DEPENDENCIES - debug_info["environment"] = ENVIRONMENT - debug_info["hagrid"] = get_version_string() - debug_info["hagrid_dev"] = EDITABLE_MODE - debug_info["hagrid_path"] = hagrid_root() - debug_info["hagrid_repo_sha"] = commit_hash() - debug_info["docker"] = docker_info() - if is_windows(): - debug_info["wsl"] = wsl_info() - debug_info["wsl_linux"] = wsl_linux_info() - return debug_info - - -def get_environment() -> dict[str, Any]: - return { - "uname": platform.uname(), - "platform": platform.system().lower(), - "os_version": platform.release(), - "python_version": platform.python_version(), - } - - -ENVIRONMENT = get_environment() - - -def os_name() -> str: - os_name = platform.system() - if os_name.lower() == "darwin": - return "macOS" - else: - return os_name - - -ENVIRONMENT["os"] = os_name() - - -def is_apple_silicon() -> bool: - if ( - "platform" in ENVIRONMENT - and ENVIRONMENT["platform"].lower() == "darwin" - and ENVIRONMENT["uname"].machine != "x86_64" - ): - return True - return False - - -ENVIRONMENT["apple_silicon"] = is_apple_silicon() - - -def is_windows() -> bool: - if "platform" in ENVIRONMENT and ENVIRONMENT["platform"].lower() == "windows": - return True - return False - - -allowed_hosts = ["docker", "azure", "aws", "gcp"] -commands = ["docker", "git", "ansible-playbook"] - -if is_windows(): - commands.append("wsl") - - -def check_deps_old() -> dict[str, str | None]: - paths = {} - for dep in commands: - paths[dep] = shutil.which(dep) - return paths - - -DEPENDENCIES = check_deps_old() - - -def docker_info() -> str: - try: - cmd = "docker info" - output = subprocess.check_output(cmd, shell=True) # nosec - return str(output.decode("utf-8")) - except Exception as e: - print("failed to get docker info", e) - return str(e) - - -def wsl_info() -> str: - try: - cmd = "wsl --status" - output = subprocess.check_output(cmd, shell=True) # nosec - return str(output.decode("utf-8")) - except Exception as e: - print("failed to get wsl info", e) - return str(e) - - -def wsl_linux_info() -> str: - try: - cmd = "wsl bash -c 'lsb_release -a'" - output = subprocess.check_output(cmd, shell=True) # nosec - return str(output.decode("utf-8")) - except Exception as e: - print("failed to get wsl linux info", e) - return str(e) - - -def check_docker_version() -> str | None: - if is_windows(): - return "N/A" # todo fix to work with windows - result = os.popen("docker compose version", "r").read() # nosec - version = None - if "version" in result: - version = result.split()[-1] - else: - print("This may be a linux machine, either that or docker compose isn't s") - print("Result:" + result) - out = subprocess.run( # nosec - ["docker", "compose"], capture_output=True, text=True - ) - if "'compose' is not a docker command" in out.stderr: - raise MissingDependency(DOCKER_ERROR) - - return version - - -def docker_running(timeout: float | None = None) -> tuple[bool, str]: - status, error_msg = False, "" - - try: - cmd = "docker info" - returncode, msg = get_cli_output(cmd, timeout=timeout) - if returncode == 0: - status, error_msg = True, "βœ… Docker service is running" - else: - error_msg = f"""❌ Docker service is either not installed or running.\n\n -To install docker, execute the following steps:\n -1 - Install docker on your machine by using the proper steps according to your OS.\n -{WHITE}MacOS: {GREEN}brew install --cask docker -{WHITE}Linux: {GREEN}curl -fsSL https://get.docker.com -o get-docker.sh && chmod +777 get-docker.sh && ./get-docker.sh -{WHITE}Windows: {GREEN}choco install docker-desktop -y{NO_COLOR} \n -2 - Run \'{GREEN}sudo usermod -a -G docker $USER\'{WHITE} to enable this user to execute docker. -3 - log out and log back in so that your group membership is re-evaluated {NO_COLOR}. --------------------------------------------------------------------------------------------------------\n -To start your docker service:\n -1 - {WHITE}MacOS/Windows: One can start docker by clicking on the "Docker" icon in your Applications folder.{NO_COLOR} -2 - {WHITE}Ubuntu: {GREEN}sudo service docker start {NO_COLOR} --------------------------------------------------------------------------------------------------------\n -""" - error_msg += f"""{YELLOW}{BOLD}Std Output Logs{NO_COLOR} -=================\n\n""" + "\n".join(msg) - - except Exception as e: # nosec - error_msg = str(e) - - return status, error_msg - - -def allowed_to_run_docker() -> tuple[bool, str]: - bool_result, msg = True, "" - if platform.system().lower() == "linux": - _, line = get_cli_output("getent group docker") - - # get user - user = getpass.getuser() - - # Check if current user is root. - if os.geteuid() == 0: - bool_result = True - - # Check if current user is member of docker group. - elif not is_gitpod() and user not in "".join(line): - msg = f"""⚠️ User is not a member of docker group. -{WHITE}You're currently not allowed to run docker, perform the following steps:\n - 1 - Run \'{GREEN}sudo usermod -a -G docker $USER\'{WHITE} to add docker permissions. - 2 - log out and log back in so that your group membership is re-evaluated {NO_COLOR}.""" - # NOTE: For some reason, inside of CI pipeline the user (runner) isn't a member of - # docker group and doesn't have sudo priviledges, but can execute docker without - # permission issues. This is just a workaround to avoid raising an exeception - # in this scenario without reason. - if user == "runner": - bool_result = True - else: - bool_result = False - - return bool_result, msg - - -def check_docker_service_status(animated: bool = True) -> None: - """Check the status of the docker service. - - Raises: - MissingDependency: If docker service is not running. - """ - - if not animated: - docker_installed, msg = docker_running(timeout=60) - user_allowed, permission_msg = allowed_to_run_docker() - else: - console = Console() - # putting \t at the end seems to prevent weird chars getting outputted - # during animations in the juypter notebook - with console.status("[bold blue]Checking for Docker Service[/bold blue]\t"): - docker_installed, msg = docker_running(timeout=60) - user_allowed, permission_msg = allowed_to_run_docker() - - # Check if user is allowed to execute docker - if not user_allowed: - raise MissingDependency(permission_msg) - - # If docker bin was not found. - if not docker_installed: - raise MissingDependency(msg) - - print("βœ… Docker service is running") - - -def check_deps( - deps: dict[str, Dependency], - of: str = "", - display: bool = True, - output_in_text: bool = False, -) -> dict[str, Dependency] | NBOutput: - output = "" - if len(of) > 0: - of = f" {of}" - # output += f"Checking{of} Dependencies:\n" - issues = [] - for dep in deps.values(): - dep.check() - output += (dep.display + "\n") if display else "" - issues += dep.issues - - if not output_in_text: - if len(issues) > 0: - output += "

🚨 Some issues were found

" - for issue in issues: - output += f"
Issue: {issue.description}
" - if issue.solution != "": - output += f"Solution:\n{issue.solution}" - if issue.command != "": - output += ( - "
Command:\n " - + f"[ ]!{issue.command}
" - ) - output += "\n" - - return NBOutput(output).to_html() - else: - if len(issues) > 0: - output += "🚨 Some issues were found\n" - for issue in issues: - output += f"\nIssue: {issue.description}\n" - if issue.solution != "": - output += f"\nSolution:\n{issue.solution}\n" - if issue.command != "": - output += "\nCommand:\n" + f"{issue.command} " - output += "\n" - - if len(output) > 0: - print(output) - return None # type: ignore - - -def check_grid_docker( - display: bool = True, output_in_text: bool = False -) -> dict[str, Dependency] | NBOutput: - try: - deps: dict[str, Dependency] = {} - deps["git"] = DependencyGridGit(name="git") - deps["docker"] = DependencyGridDocker(name="docker") - deps["docker_compose"] = DependencyGridDockerCompose(name="docker compose") - return check_deps( - of="Grid", deps=deps, display=display, output_in_text=output_in_text - ) - except Exception as e: - try: - if display and not output_in_text: - return NBOutput(debug_exception(e=e)).to_html() - except Exception: # nosec - pass - - print(e) - raise e - - -def debug_exception(e: Exception) -> str: - exception = ( - f'
An exception occured: {e}.
' - + "Please file a bug report on GitHub Issues or in Slack #support
" - ) - exception += "\n" - exception += ".\n" - exception += "https://slack.openmined.org/\n" - exception += "https://github.com/OpenMined/PySyft/issues\n" - exception += "\n\nWhen reporting bugs, please copy everything between the lines.\n" - exception += "==================================================================\n" - exception += ( - "" + json.dumps(gather_debug(), indent=4, sort_keys=True) + "" - ) - exception += "\n" - exception += traceback.format_exc() - exception += ( - "\n=================================================================\n\n" - ) - return exception - - -def check_syft_deps(display: bool = True) -> dict[str, Dependency] | NBOutput: - try: - deps: dict[str, Dependency] = {} - deps["os"] = DependencySyftOS(name="os") - deps["python"] = DependencySyftPython(name="python") - return check_deps(of="Syft", deps=deps, display=display) - except Exception as e: - try: - if display: - return NBOutput(debug_exception(e=e)).to_html() - except Exception: # nosec - pass - - print(e) - raise e - - -def check_hagrid(display: bool = True) -> dict[str, Dependency] | NBOutput: - try: - deps: dict[str, Dependency] = {} - deps["hagrid"] = DependencyPyPI( - package_name="hagrid", - package_display_name="HAGrid", - update_available_issue=hagrid_update_available, - ) - return check_deps(deps=deps, display=display) - except Exception as e: - try: - if display: - return NBOutput(debug_exception(e=e)).to_html() - except Exception: # nosec - pass - - print(e) - raise e - - -def check_syft( - display: bool = True, pre: bool = False -) -> dict[str, Dependency] | NBOutput: - try: - deps: dict[str, Dependency] = {} - deps["os"] = DependencySyftOS(name="os") - deps["python"] = DependencySyftPython(name="python") - deps["syft"] = DependencyPyPI( - package_name="syft", - package_display_name="Syft", - pre=pre, - install_issue=syft_install, - update_available_issue=syft_update_available, - ) - return check_deps(deps=deps, display=display) - except Exception as e: - try: - if display: - return NBOutput(debug_exception(e=e)).to_html() - except Exception: # nosec - pass - - print(e) - raise e - - -PACKAGE_MANAGER_COMMANDS = { - "git": { - "macos": "brew install git", - "windows": 'choco install git.install --params "/GitAndUnixToolsOnPath /WindowsTerminal /NoAutoCrlf" -y', - "linux": "sudo apt update && sudo apt install git", - "backup_url": "https://git-scm.com/downloads", - }, - "docker": { - "macos": "brew install --cask docker", - "windows": "choco install docker-desktop -y", - "linux": "curl -fsSL https://get.docker.com -o get-docker.sh && chmod +777 get-docker.sh && ./get-docker.sh", - "backup_url": "https://www.docker.com/products/docker-desktop/", - }, - "docker_compose": { - "macos": "brew install --cask docker", - "windows": "choco install docker-desktop -y", - "linux": ( - "mkdir -p ~/.docker/cli-plugins\n" - + "DOCKER_COMPOSE_VERSION=v2.21.0\n" - + "curl -sSL https://github.com/docker/compose/releases/download/" - + "${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 " - + "-o ~/.docker/cli-plugins/docker-compose\n" - + "chmod +x ~/.docker/cli-plugins/docker-compose" - ), - "backup_url": "https://github.com/docker/compose", - }, -} - -PACKAGE_MANAGERS = { - "macos": "brew", - "windows": "choco", - "linux": "apt", -} - - -def os_package_manager_install_cmd( - package_name: str, package_display_name: str, output_in_text: bool = False -) -> tuple[str | None, str | None]: - os = ENVIRONMENT["os"].lower() - cmd = None - url = None - package_manager = PACKAGE_MANAGERS[os] - if ( - package_name in PACKAGE_MANAGER_COMMANDS - and os in PACKAGE_MANAGER_COMMANDS[package_name] - ): - cmd = PACKAGE_MANAGER_COMMANDS[package_name][os] - if ( - package_name in PACKAGE_MANAGER_COMMANDS - and "backup_url" in PACKAGE_MANAGER_COMMANDS[package_name] - ): - url = PACKAGE_MANAGER_COMMANDS[package_name]["backup_url"] - - solution = "" - - if not output_in_text: - if cmd: - solution += f"- You can install {package_display_name} with {package_manager}\n" - if url: - if cmd: - solution += "- Alternatively, you " - else: - solution += "- You " - solution += f"can download and install {package_display_name}" - solution += f'from {url}' - else: - if cmd: - solution += ( - f"- You can install {package_display_name} with {package_manager}\n" - ) - if url: - if cmd: - solution += "- Alternatively, you " - else: - solution += "- You " - solution += f"can download and install {package_display_name} from {url}" - - return (cmd, solution) - - -def docker_compose_install() -> SetupIssue: - command, solution = os_package_manager_install_cmd( - package_name="docker_compose", package_display_name="Docker Compose" - ) - return SetupIssue( - issue_name="docker_compose_install", - description="You do not have Docker Compose v2 installed.", - command=command, - solution=solution, - ) - - -def docker_install() -> SetupIssue: - command, solution = os_package_manager_install_cmd( - package_name="docker", package_display_name="Docker" - ) - return SetupIssue( - issue_name="docker_install", - description="You do not have Docker installed.", - command=command, - solution=solution, - ) - - -def git_install(output_in_text: bool = False) -> SetupIssue: - command, solution = os_package_manager_install_cmd( - package_name="git", package_display_name="Git", output_in_text=output_in_text - ) - return SetupIssue( - issue_name="git_install", - description="You do not have Git installed.", - command=command, - solution=solution, - ) - - -def syft_install(pre: bool = False) -> SetupIssue: - command = "pip install -U syft" - if pre: - # command += " --pre" - pass - return SetupIssue( - issue_name="syft_install", - description="You have not installed Syft.", - command=command, - solution="You can install Syft with pip.", - ) - - -def syft_update_available(current_version: Version, new_version: Version) -> SetupIssue: - return SetupIssue( - issue_name="syft_update_available", - description=( - "A new release of Syft is available: " - + f"{str(current_version)} -> {str(new_version)}." - ), - command=f"pip install syft=={new_version}", - solution="You can upgrade Syft with pip.", - ) - - -def hagrid_update_available( - current_version: Version, new_version: Version -) -> SetupIssue: - return SetupIssue( - issue_name="hagrid_update_available", - description=( - "A new release of HAGrid is available: " - + f"{str(current_version)} -> {str(new_version)}." - ), - command=f"pip install -U hagrid=={new_version}", - solution="You can upgrade HAGrid with pip.", - ) - - -def python_version_unsupported() -> SetupIssue: - return SetupIssue( - issue_name="python_version_unsupported", - description=( - f"Syft supports Python >= {SYFT_MINIMUM_PYTHON_VERSION_STRING} " - + f"and <= {SYFT_MAXIMUM_PYTHON_VERSION_STRING}" - ), - command="", - solution="You must install a compatible version of Python", - ) diff --git a/packages/hagrid/hagrid/dummynum.py b/packages/hagrid/hagrid/dummynum.py deleted file mode 100644 index 06b28f28682..00000000000 --- a/packages/hagrid/hagrid/dummynum.py +++ /dev/null @@ -1,28 +0,0 @@ -# stdlib -from typing import Any - -# a dummy enum - - -class Meta(type): - # any property returns another dummy which can also be executed - def __getattribute__(cls, name: str) -> Any: - try: - return super().__getattribute__(name) - except Exception: # nosec - pass - return return_dummy() - - -# this lets us prevent runtime errors of missing types in older syft -class DummyNum(metaclass=Meta): - def __init__(self, *args: Any, **kwargs: Any) -> None: - pass - - def __call__(self, *args: Any, **kwargs: Any) -> Any: - return self - - -def return_dummy() -> DummyNum: - # this lets us create the sub class in the parent meta on getattr - return DummyNum() diff --git a/packages/hagrid/hagrid/exceptions.py b/packages/hagrid/hagrid/exceptions.py deleted file mode 100644 index 0bb68a0dd55..00000000000 --- a/packages/hagrid/hagrid/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class MissingDependency(Exception): - pass diff --git a/packages/hagrid/hagrid/file.py b/packages/hagrid/hagrid/file.py deleted file mode 100644 index 378f5fdcf10..00000000000 --- a/packages/hagrid/hagrid/file.py +++ /dev/null @@ -1,8 +0,0 @@ -# stdlib -import os - - -def user_hagrid_profile() -> str: - dir_path = os.path.expanduser("~/.hagrid") - os.makedirs(dir_path, exist_ok=True) - return os.path.abspath(dir_path) diff --git a/packages/hagrid/hagrid/git_check.py b/packages/hagrid/hagrid/git_check.py deleted file mode 100644 index c98028f0c52..00000000000 --- a/packages/hagrid/hagrid/git_check.py +++ /dev/null @@ -1,15 +0,0 @@ -# relative -from .deps import DependencyGridGit -from .deps import check_deps - - -def verify_git_installation() -> None: - dep = DependencyGridGit(name="git", output_in_text=True) - deps = {} - deps["git"] = dep - check_deps(of="Git", deps=deps, display=False, output_in_text=True) # type: ignore - if dep.issues: - exit(1) - - -verify_git_installation() diff --git a/packages/hagrid/hagrid/grammar.py b/packages/hagrid/hagrid/grammar.py deleted file mode 100644 index 62f98d47fe8..00000000000 --- a/packages/hagrid/hagrid/grammar.py +++ /dev/null @@ -1,365 +0,0 @@ -# future -from __future__ import annotations - -# stdlib -from collections.abc import Callable -import socket -from typing import Any - -# relative -from .deps import allowed_hosts -from .lib import find_available_port - -ALLOWED_NODE_TYPES = ["domain", "network", "gateway", "enclave"] - - -class BadGrammar(Exception): - pass - - -class GrammarVerb: - def __init__( - self, - command: str, - full_sentence: list[dict[str, Any]], - abbreviations: dict[int, list[str | None]], - ) -> None: - self.grammar: list[GrammarTerm | HostGrammarTerm | SourceGrammarTerm] = [] - self.command = command - self.full_sentence = full_sentence - self.abbreviations = abbreviations - - def get_named_term_grammar(self, name: str) -> GrammarTerm: - for term in self.grammar: - if term.name == name and isinstance(term, GrammarTerm): - return term - raise BadGrammar(f"GrammarTerm with {name} not found in {self.grammar}") - - def get_named_term_hostgrammar(self, name: str) -> HostGrammarTerm: - for term in self.grammar: - if term.name == name and isinstance(term, HostGrammarTerm): - return term - raise BadGrammar(f"HostGrammarTerm with {name} not found in {self.grammar}") - - def get_named_term_type( - self, name: str, term_type: str | None = None - ) -> GrammarTerm | HostGrammarTerm: - if term_type == "host": - return self.get_named_term_hostgrammar(name=name) - return self.get_named_term_grammar(name=name) - - def set_named_term_type( - self, name: str, new_term: GrammarTerm, term_type: str | None = None - ) -> None: - new_grammar = [] - for term in self.grammar: - found = False - if term.name == name: - if term_type is not None and term.type == term_type: - found = True - elif term_type is None: - found = True - if not found: - new_grammar.append(term) - else: - new_grammar.append(new_term) - self.grammar = new_grammar - - def load_grammar( - self, grammar: list[GrammarTerm | HostGrammarTerm | SourceGrammarTerm] - ) -> None: - self.grammar = grammar - - -class GrammarTerm: - def __init__( - self, - type: str, - name: str, - default: str | Callable | None = None, - options: list | None = None, - example: str | None = None, - **kwargs: Any, - ) -> None: - self.raw_input: str | None = None - self.input: str | None = None - self.type = type - self.name = name - self.default = default - self.options = options if options is not None else [] - self.example = example - - @property - def snake_input(self) -> str | None: - if self.input: - return self.input.lower().replace(" ", "_") - return None - - @property - def kebab_input(self) -> str | None: - if self.input: - return self.input.lower().replace(" ", "-") - return None - - def __repr__(self) -> str: - return f"<{type(self).__name__}: {self.name}<{self.type}>: {self.input} [raw: {self.raw_input}]>" - - def get_example(self) -> str: - return_value = self.example if self.example else self.default - if callable(return_value): - return_value = return_value() - return str(return_value) - - # no op - def custom_parsing(self, input: str) -> str: - return input - - def parse_input(self, input: str | None) -> None: - self.raw_input = input - if input is None and self.default is None: - raise BadGrammar( - f"{self.name} has no default, please use one of the following options: {self.options}" - ) - if input is None: - if isinstance(self.default, str): - input = self.default - elif callable(self.default): - input = self.default() - - if len(self.options) > 0 and input not in self.options: - raise BadGrammar( - f"{input} is not valid for {self.name} please use one of the following options: {self.options}" - ) - - self.input = self.custom_parsing(input=input) if input else input - - -class HostGrammarTerm(GrammarTerm): - @property - def host(self) -> str | None: - return self.parts()[0] - - @property - def port(self) -> int | None: - return self.parts()[1] - - @property - def search(self) -> bool: - return bool(self.parts()[2]) - - @property - def port_tls(self) -> int: - if self.port == 80: - return 443 - return 444 - - @property - def free_port(self) -> int: - if self.port is None: - raise BadGrammar( - f"{type(self)} unable to check if port {self.port} is free" - ) - return find_available_port(host="localhost", port=self.port, search=self.search) - - @property - def free_port_tls(self) -> int: - if self.port_tls is None: - raise BadGrammar( - f"{type(self)} unable to check if tls port {self.port_tls} is free" - ) - return find_available_port(host="localhost", port=self.port_tls, search=True) - - def parts(self) -> tuple[str | None, int | None, bool]: - host = None - port: int | None = None - search = False - if self.input: - parts = self.input.split(":") - host = parts[0] - if len(parts) > 1: - port_str = parts[1] - if port_str.endswith("+"): - search = True - port_str = port_str[0:-1] - port = int(port_str) - return (host, port, search) - - def validate_host(self, host_or_ip: str) -> bool: - try: - if socket.gethostbyname(host_or_ip) == host_or_ip: - return True - elif socket.gethostbyname(host_or_ip) != host_or_ip: - return True - except socket.gaierror: - raise BadGrammar( - f"{host_or_ip} is not valid for {self.name}. Try an IP, hostname or docker, vm, aws, azure or gcp" - ) - return False - - def validate_port(self, port: str) -> bool: - try: - if port.endswith("+"): - int(port[0:-1]) - else: - int(port) - except Exception: # nosec - raise BadGrammar( - f"{port} is not a valid port option. Try: {self.get_example()}" - ) - return True - - def custom_parsing(self, input: str) -> str: - colons = input.count(":") - host = input - port = None - if colons > 1: - raise BadGrammar( - f"You cannot have more than one : for {self.name}, try: {self.get_example()}" - ) - elif colons == 1: - parts = input.split(":") - host = parts[0] - port = parts[1] - - if port is None: - if host == "docker": - port = "8081+" # default - else: - port = "80" # default - - if host not in allowed_hosts: - _ = self.validate_host(host_or_ip=host) - - _ = self.validate_port(port=port) - - return f"{host}:{port}" - - -class SourceGrammarTerm(GrammarTerm): - def custom_parsing(self, input: str) -> str: - trimmed = input - if trimmed.startswith("http://"): - trimmed = trimmed.replace("http://", "") - if trimmed.startswith("https://"): - trimmed = trimmed.replace("https://", "") - if trimmed.startswith("github.com/"): - trimmed = trimmed.replace("github.com/", "") - - parts = trimmed.split("/") - if "tree" not in input or len(parts) < 4: - raise BadGrammar( - f"{self.name} should be a valid github.com repo branch url. Try: {self.get_example()}" - ) - - repo = f"{parts[0]}/{parts[1]}" - branch = "/".join(parts[3:]) - - return f"{repo}:{branch}" - - -def validate_arg_count(arg_count: int, verb: GrammarVerb) -> bool: - valid = True - - if arg_count not in verb.abbreviations: - error_str = f"Command {verb.command} supports the following invocations:\n" - for count in sorted(verb.abbreviations.keys()): - abbreviation = verb.abbreviations[count] - example_terms = [] - for i, term_type in enumerate(abbreviation): - if term_type is not None: - term_settings = verb.full_sentence[i] - example = term_settings["klass"](**term_settings).get_example() - example_terms.append(example) - error_str += f"{count} args: {verb.command} {' '.join(example_terms)}\n" - - raise BadGrammar(error_str) - - return valid - - -def launch_shorthand_support(args: tuple) -> tuple: - """When launching, we want to be able to default to 'domain' if it's not provided, to launch - nodes when no name is provided, and to support node names which have multiple words. - - hagrid launch -> hagrid launch domain - hagrid launch United Nations -> hagrid launch "United Nations" domain - hagrid launch United Nations domain -> hagrid launch "United Nations" domain - hagrid launch on docker -> hagrid launch domain on docker - - """ - - # Some mild analysis - found_node_type = False - preposition_position = 10000 - for i, arg in enumerate(args): - if arg in ALLOWED_NODE_TYPES: - found_node_type = True - - if arg.strip() in ["to", "from"]: - if i < preposition_position: - preposition_position = i - - _args = list(args) - - # Default to domain if it's not provided - if not found_node_type: - if preposition_position != 10000: - _args.insert(preposition_position, "domain") - preposition_position += 1 - else: - _args = _args + ["domain"] - - # if there are no prepositions and the domain/network is the last word - if preposition_position == 10000 and _args[-1] in ALLOWED_NODE_TYPES: - _args = [" ".join(_args[:-1])] + _args[-1:] - - # if there are prepositions then combine the words in the name if there are multiple - elif preposition_position != 10000: - _args = [" ".join(_args[: preposition_position - 1])] + _args[ - preposition_position - 1 : - ] - - # if there wasn't a name provided - make sure we don't have an empty place in the list - # so that later logic will generate a name - if _args[0] == "": - _args = _args[1:] - - args = tuple(_args) - - return args - - -def parse_grammar(args: tuple, verb: GrammarVerb) -> list[GrammarTerm]: - # if the command is a launch, check if any shorthands were employed - if verb.command == "launch": - args = launch_shorthand_support(args=args) - - arg_list = list(args) - arg_count = len(arg_list) - errors = [] - if validate_arg_count(arg_count=arg_count, verb=verb): - terms = [] - abbreviation = verb.abbreviations[arg_count] - for i, term_type in enumerate(abbreviation): - if term_type is None: - arg = None # use None so we get the default - else: - arg = arg_list.pop(0) # use a real arg - - term_settings = verb.full_sentence[i] - - try: - term = term_settings["klass"](**term_settings) - term.parse_input(arg) - terms.append(term) - - except BadGrammar as e: - errors.append(str(e)) - - if len(errors) > 0: - raise BadGrammar("\n".join(errors)) - - # make command - return terms - else: - raise BadGrammar("Grammar is not valid") diff --git a/packages/hagrid/hagrid/img/hagrid.png b/packages/hagrid/hagrid/img/hagrid.png deleted file mode 100644 index 2b4dbd75d41..00000000000 Binary files a/packages/hagrid/hagrid/img/hagrid.png and /dev/null differ diff --git a/packages/hagrid/hagrid/img/hagrid2.png b/packages/hagrid/hagrid/img/hagrid2.png deleted file mode 100644 index 8df9d58d147..00000000000 Binary files a/packages/hagrid/hagrid/img/hagrid2.png and /dev/null differ diff --git a/packages/hagrid/hagrid/land.py b/packages/hagrid/hagrid/land.py deleted file mode 100644 index 1c138c1971b..00000000000 --- a/packages/hagrid/hagrid/land.py +++ /dev/null @@ -1,60 +0,0 @@ -# stdlib - -# relative -from .grammar import GrammarTerm -from .grammar import GrammarVerb -from .grammar import HostGrammarTerm - - -def get_land_verb() -> GrammarVerb: - full_sentence = [ - { - "name": "node_name", - "type": "adjective", - "klass": GrammarTerm, - "example": "'my_domain'", - }, - { - "name": "preposition", - "type": "preposition", - "klass": GrammarTerm, - "default": "at", - "options": ["at", "on"], - }, - { - "name": "host", - "type": "propernoun", - "klass": HostGrammarTerm, - "default": "docker", - "example": "docker", - }, - ] - - abbreviations: dict[int, list[str | None]] = { - 3: [ - "adjective", - "preposition", - "propernoun", - ], # node_name # at # host - 2: [ - "adjective", - None, - "propernoun", - ], # node_name # ignore # host - 1: [ - "adjective", - None, - None, - ], # node_name # ignore # ignore - 0: [ - None, - None, - None, - ], # ignore # ignore # ignore - } - - return GrammarVerb( - command="land", - full_sentence=full_sentence, - abbreviations=abbreviations, - ) diff --git a/packages/hagrid/hagrid/launch.py b/packages/hagrid/hagrid/launch.py deleted file mode 100644 index c6cc785da50..00000000000 --- a/packages/hagrid/hagrid/launch.py +++ /dev/null @@ -1,113 +0,0 @@ -# stdlib - -# relative -from .cache import DEFAULT_BRANCH -from .grammar import ALLOWED_NODE_TYPES -from .grammar import GrammarTerm -from .grammar import GrammarVerb -from .grammar import HostGrammarTerm -from .grammar import SourceGrammarTerm -from .names import random_name - - -def get_launch_verb() -> GrammarVerb: - full_sentence = [ - { - "name": "node_name", - "type": "propernoun", - "klass": GrammarTerm, - "default": random_name, - "example": "'my_domain'", - }, - { - "name": "node_type", - "type": "object", - "klass": GrammarTerm, - "default": "domain", - "options": ALLOWED_NODE_TYPES, - }, - { - "name": "preposition", - "type": "preposition", - "klass": GrammarTerm, - "default": "to", - "options": ["to"], - }, - { - "name": "host", - "type": "propernoun", - "klass": HostGrammarTerm, - "default": "docker", - "example": "docker:8081+", - }, - { - "name": "preposition", - "type": "preposition", - "klass": GrammarTerm, - "default": "from", - "options": ["from"], - }, - { - "name": "source", - "type": "propernoun", - "klass": SourceGrammarTerm, - "default": f"github.com/OpenMined/PySyft/tree/{DEFAULT_BRANCH}", - }, - ] - - abbreviations: dict[int, list[str | None]] = { - 6: [ - "propernoun", # name - "object", # node_type - "preposition", # to - "propernoun", # host - "preposition", # from - "propernoun", # source - ], - 5: [ - None, # name - "object", # node_type - "preposition", # to - "propernoun", # host - "preposition", # from - "propernoun", # source - ], - 4: [ - "propernoun", # name - "object", # node_type - "preposition", # to - "propernoun", # host - None, # ignore - None, # ignore - ], - 3: [ - None, # ignore - "object", # node_type - "preposition", # to - "propernoun", # host - None, # ignore - None, # ignore - ], - 2: [ - "propernoun", # name - "object", # node_type - None, # ignore - None, # ignore - None, # ignore - None, # ignore - ], - 1: [ - None, # ignore - "object", # node_type - None, # ignore - None, # ignore - None, # ignore - None, # ignore - ], - } - - return GrammarVerb( - command="launch", - full_sentence=full_sentence, - abbreviations=abbreviations, - ) diff --git a/packages/hagrid/hagrid/lib.py b/packages/hagrid/hagrid/lib.py deleted file mode 100644 index 057f77160f7..00000000000 --- a/packages/hagrid/hagrid/lib.py +++ /dev/null @@ -1,472 +0,0 @@ -# stdlib -from enum import Enum -import hashlib -import importlib -import importlib.machinery -import importlib.util -import json -import os -from pathlib import Path -import random -import shutil -import socket -import subprocess # nosec - -# third party -import git -import requests -import rich -from rich import console -from rich import progress -from rich.table import Table - -# relative -from .cache import DEFAULT_BRANCH -from .mode import EDITABLE_MODE -from .mode import hagrid_root - - -class GitRemoteProgress(git.RemoteProgress): - # CREDITS: https://splunktool.com/python-progress-bar-for-git-clone - OP_CODES = [ - "BEGIN", - "CHECKING_OUT", - "COMPRESSING", - "COUNTING", - "END", - "FINDING_SOURCES", - "RECEIVING", - "RESOLVING", - "WRITING", - ] - OP_CODE_MAP = { - getattr(git.RemoteProgress, _op_code): _op_code for _op_code in OP_CODES - } - - def __init__(self) -> None: - super().__init__() - self.progressbar = progress.Progress( - progress.SpinnerColumn(), - # *progress.Progress.get_default_columns(), - progress.TextColumn("[progress.description]{task.description}"), - progress.BarColumn(), - progress.TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - "eta", - progress.TimeRemainingColumn(), - progress.TextColumn("{task.fields[message]}"), - console=console.Console(), - transient=False, - ) - self.progressbar.start() - self.active_task = None - - def __del__(self) -> None: - # logger.info("Destroying bar...") - self.progressbar.stop() - - @classmethod - def get_curr_op(cls, op_code: int) -> str: - """Get OP name from OP code.""" - # Remove BEGIN- and END-flag and get op name - op_code_masked = op_code & cls.OP_MASK - return cls.OP_CODE_MAP.get(op_code_masked, "?").title() - - def update( - self, - op_code: int, - cur_count: str | float, - max_count: str | float | None = None, - message: str | None = None, - ) -> None: - # Start new bar on each BEGIN-flag - if op_code & self.BEGIN: - self.curr_op = self.get_curr_op(op_code) - # logger.info("Next: %s", self.curr_op) - self.active_task = self.progressbar.add_task( - description=self.curr_op, - total=max_count, - message=message, - ) - - self.progressbar.update( - task_id=self.active_task, - completed=cur_count, - message=message, - ) - - # End progress monitoring on each END-flag - if op_code & self.END: - # logger.info("Done: %s", self.curr_op) - self.progressbar.update( - task_id=self.active_task, - message=f"[bright_black]{message}", - ) - - -class ProcessStatus(Enum): - RUNNING = "[blue]Running" - DONE = "[green]Done" - FAILED = "[red]Failed" - - -def docker_desktop_memory() -> int: - path = str(Path.home()) + "/Library/Group Containers/group.com.docker/settings.json" - - try: - f = open(path) - out = f.read() - f.close() - return json.loads(out)["memoryMiB"] - - except Exception: # nosec - # docker desktop not found - probably running linux - return -1 - - -def asset_path() -> os.PathLike: - return Path(hagrid_root()) / "hagrid" - - -def manifest_template_path() -> os.PathLike: - return Path(asset_path()) / "manifest_template.yml" - - -def hagrid_cache_dir() -> os.PathLike: - return Path("~/.hagrid").expanduser() - - -def repo_src_path() -> Path: - if EDITABLE_MODE: - return Path(os.path.abspath(Path(hagrid_root()) / "../../")) - else: - return Path(os.path.join(Path(hagrid_cache_dir()) / "PySyft")) - - -def grid_src_path() -> str: - return str(repo_src_path() / "packages" / "grid") - - -def check_is_git(path: Path) -> bool: - is_repo = False - try: - git.Repo(path) - is_repo = True - except Exception: # nosec - pass - return is_repo - - -def is_gitpod() -> bool: - return bool(os.environ.get("GITPOD_WORKSPACE_URL", None)) - - -def gitpod_url(port: int | None = None) -> str: - workspace_url = os.environ.get("GITPOD_WORKSPACE_URL", "") - if port: - workspace_url = workspace_url.replace("https://", f"https://{port}-") - return workspace_url - - -def get_git_repo() -> git.Repo: - # relative - from .art import RichEmoji - - OK_EMOJI = RichEmoji("white_heavy_check_mark").to_str() - - is_git = check_is_git(path=repo_src_path()) - console = rich.get_console() - - if not EDITABLE_MODE and not is_git: - github_repo = "OpenMined/PySyft.git" - git_url = f"https://github.com/{github_repo}" - - print(f"Fetching Syft + Grid Source from {git_url} to {repo_src_path()}") - try: - repo_branch = DEFAULT_BRANCH - repo_path = repo_src_path() - - if repo_path.exists(): - shutil.rmtree(str(repo_path)) - - git.Repo.clone_from( - git_url, - str(repo_path), - single_branch=False, - b=repo_branch, - progress=GitRemoteProgress(), - ) - console.print(f"{OK_EMOJI} Fetched PySyft repo.") - except Exception as e: # nosec - print(f"Failed to clone {git_url} to {repo_src_path()} with error: {e}") - return git.Repo(repo_src_path()) - - -def update_repo(repo: git.Repo, branch: str) -> None: - # relative - from .art import RichEmoji - - OK_EMOJI = RichEmoji("white_heavy_check_mark").to_str() - console = rich.get_console() - if not EDITABLE_MODE: - with console.status("Updating hagrid") as console_status: - console_status.update(f"[bold blue]Updating HAGrid from branch: {branch}") - try: - if repo.is_dirty(): - repo.git.reset("--hard") - repo.remotes.origin.fetch() - repo.git.checkout(branch) - repo.remotes.origin.pull() - console.print(f"{OK_EMOJI} Updated HAGrid from branch: {branch}") - except Exception as e: - print(f"Error checking out branch {branch}.", e) - - -def commit_hash() -> str: - try: - repo = get_git_repo() - sha = repo.head.commit.hexsha - return sha - except Exception as e: - print("failed to get repo sha", e) - return "unknown" - - -def use_branch(branch: str) -> None: - if not EDITABLE_MODE: - print(f"Using HAGrid from branch: {branch}") - repo = get_git_repo() - try: - if repo.is_dirty(): - repo.git.reset("--hard") - repo.remotes.origin.fetch() - repo.git.checkout(branch) - repo.remotes.origin.pull() - except Exception as e: - print(f"Error checking out branch {branch}.", e) - - -def should_provision_remote( - username: str | None, password: str | None, key_path: str | None -) -> bool: - is_remote = username is not None or password is not None or key_path is not None - if username and password or username and key_path: - return is_remote - if is_remote: - raise Exception("--username requires either --password or --key-path") - return is_remote - - -def name_tag(name: str) -> str: - return hashlib.sha256(name.encode("utf8")).hexdigest() - - -def find_available_port( - host: str, port: int | None = None, search: bool = False -) -> int: - if port is None: - port = random.randint(1500, 65000) # nosec - port_available = False - while not port_available: - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result_of_check = sock.connect_ex((host, port)) - - if result_of_check != 0: - port_available = True - break - else: - if search: - port += 1 - else: - break - sock.close() - - except Exception as e: - print(f"Failed to check port {port}. {e}") - sock.close() - - if search is False and port_available is False: - error = ( - f"{port} is in use, either free the port or " - + f"try: {port}+ to auto search for a port" - ) - raise Exception(error) - return port - - -def get_version_module() -> tuple[str, str]: - try: - version_file_path = f"{grid_src_path()}/VERSION" - loader = importlib.machinery.SourceFileLoader("VERSION", version_file_path) - spec = importlib.util.spec_from_loader(loader.name, loader) - if spec: - version_module = importlib.util.module_from_spec(spec) - loader.exec_module(version_module) - version = version_module.get_version() - hash = version_module.get_hash() - return (version, hash) - except Exception as e: - print(f"Failed to retrieve versions from: {version_file_path}. {e}") - return ("unknown", "unknown") - - -# Check base route of an IP address -def check_host(ip: str, silent: bool = False) -> bool: - try: - socket.gethostbyname(ip) - return True - except Exception as e: - if not silent: - print(f"Failed to resolve host {ip}. {e}") - return False - - -# Check status of login page -def check_login_page(ip: str, timeout: int = 30, silent: bool = False) -> bool: - try: - url = f"http://{ip}/login" - response = requests.get(url, timeout=timeout) - if response.status_code == 200: - return True - else: - return False - except Exception as e: - if not silent: - print(f"Failed to check login page {ip}. {e}") - return False - - -# Check api metadata -def check_api_metadata(ip: str, timeout: int = 30, silent: bool = False) -> bool: - try: - url = f"http://{ip}/api/v2/metadata" - response = requests.get(url, timeout=timeout) - if response.status_code == 200: - return True - else: - return False - except Exception as e: - if not silent: - print(f"Failed to check api metadata {ip}. {e}") - return False - - -def save_vm_details_as_json(username: str, password: str, process_list: list) -> None: - """Saves the launched hosts details as json.""" - - host_ip_details: list = [] - - # file path to save host details - dir_path = os.path.expanduser("~/.hagrid") - os.makedirs(dir_path, exist_ok=True) - file_path = f"{dir_path}/host_ips.json" - - for ip_address, _, jupyter_token in process_list: - _data = { - "username": username, - "password": password, - "ip_address": ip_address, - "jupyter_token": jupyter_token, - } - host_ip_details.append(_data) - - # save host details - with open(file_path, "w") as fp: - json.dump({"host_ips": host_ip_details}, fp) - - print(f"Saved vm details at: {file_path}") - - -def generate_user_table(username: str, password: str) -> Table | str: - if not username and not password: - return "" - - table = Table(title="Virtual Machine Credentials") - table.add_column("Username") - table.add_column("Password") - - table.add_row(f"[green]{username}", f"[green]{password}") - - return table - - -def get_process_status(process: subprocess.Popen) -> str: - poll_status = process.poll() - if poll_status is None: - return ProcessStatus.RUNNING.value - elif poll_status != 0: - return ProcessStatus.FAILED.value - else: - return ProcessStatus.DONE.value - - -def generate_process_status_table(process_list: list) -> tuple[Table, bool]: - """Generate a table to show the status of the processes being exected. - - Args: - process_list (list): each item in the list - is a tuple of ip_address, process and jupyter token - - Returns: - Tuple[Table, bool]: table of process status and flag to indicate if all processes are executed. - """ - - process_statuses: list[str] = [] - lines_to_display = 5 # Number of lines to display as output - - table = Table(title="Virtual Machine Status") - table.add_column("PID", style="cyan") - table.add_column("IpAddress", style="magenta") - table.add_column("Status") - table.add_column("Jupyter Token", style="white on black") - table.add_column("Log", overflow="fold", no_wrap=False) - - for ip_address, process, jupyter_token in process_list: - process_status = get_process_status(process) - - process_statuses.append(process_status) - - process_log = [] - if process_status == ProcessStatus.FAILED.value: - process_log += process.stderr.readlines(lines_to_display) - else: - process_log += process.stdout.readlines(lines_to_display) - - process_log_str = "\n".join(log.decode("utf-8") for log in process_log) - process_log_str = process_log_str if process_log else "-" - - table.add_row( - f"{process.pid}", - f"{ip_address}", - f"{process_status}", - f"{jupyter_token}", - f"{process_log_str}", - ) - - processes_completed = ProcessStatus.RUNNING.value not in process_statuses - - return table, processes_completed - - -def check_jupyter_server( - host_ip: str, wait_time: int = 5, silent: bool = False -) -> bool: - if not silent: - print(f"Checking Jupyter Server at VM {host_ip} is up") - - try: - url = f"http://{host_ip}:8888/" - response = requests.get(url, timeout=wait_time) - if response.status_code == 200: - return True - else: - return False - except Exception as e: - if not silent: - print(f"Failed to check jupyter server status {host_ip}. {e}") - return False - - -GIT_REPO = get_git_repo -GRID_SRC_VERSION = get_version_module -GRID_SRC_PATH = grid_src_path diff --git a/packages/hagrid/hagrid/manifest_template.yml b/packages/hagrid/hagrid/manifest_template.yml deleted file mode 100644 index 552fdb3efd7..00000000000 --- a/packages/hagrid/hagrid/manifest_template.yml +++ /dev/null @@ -1,31 +0,0 @@ -manifestVersion: 0.1 -hagrid_version: 0.3.121 -syft_version: 0.8.7-beta.7 -dockerTag: 0.8.7-beta.7 -baseUrl: https://raw.githubusercontent.com/OpenMined/PySyft/ -hash: 4333433d5bec7bb9bcd52db59029d3bcb23c74c2 -target_dir: ~/.hagrid/PySyft/ -files: - grid: - path: packages/grid/ - common: - - default.env - docker: - - default.env - - docker-compose.build.yml - - docker-compose.dev.yml - - docker-compose.pull.yml - - docker-compose.test.yml - - docker-compose.tls.yml - - docker-compose.yml - - traefik/docker/dynamic-tls.yml - - traefik/docker/dynamic.yml - - traefik/docker/traefik-tls.template.yml - - traefik/docker/traefik.yml - k8s: - - devspace.yaml - podman: - - podman/podman-kube/podman-syft-kube-config.yaml - - podman/podman-kube/podman-syft-kube.yaml - - podman/podman-kube/traefik/conf/dynamic.yml - - podman/podman-kube/traefik/traefik.yml diff --git a/packages/hagrid/hagrid/mode.py b/packages/hagrid/hagrid/mode.py deleted file mode 100644 index e21da8ccbba..00000000000 --- a/packages/hagrid/hagrid/mode.py +++ /dev/null @@ -1,47 +0,0 @@ -# stdlib -import os -from pathlib import Path -import site - - -def str_to_bool(bool_str: str | None) -> bool: - result = False - bool_str = str(bool_str).lower() - if bool_str == "true" or bool_str == "1": - result = True - return result - - -def hagrid_root() -> str: - return os.path.abspath(str(Path(__file__).parent.parent)) - - -def is_editable_mode() -> bool: - disable_editable_mode = str_to_bool( - os.environ.get("DISABLE_EDITABLE_MODE", "False") - ) - if disable_editable_mode: - print("🚨 Editable Mode DISABLED") - return False - current_package_root = hagrid_root() - - installed_as_editable = False - sitepackages_dirs = site.getsitepackages() - # check all site-packages returned if they have a hagrid.egg-link - for sitepackages_dir in sitepackages_dirs: - egg_link_file = Path(sitepackages_dir) / "hagrid.egg-link" - try: - linked_folder = egg_link_file.read_text() - # if the current code is in the same path as the egg-link its -e mode - installed_as_editable = current_package_root in linked_folder - break - except Exception: # nosec - pass - - if os.path.exists(Path(current_package_root) / "hagrid.egg-info"): - installed_as_editable = True - - return installed_as_editable - - -EDITABLE_MODE = is_editable_mode() diff --git a/packages/hagrid/hagrid/names.py b/packages/hagrid/hagrid/names.py deleted file mode 100644 index 26e6b92025d..00000000000 --- a/packages/hagrid/hagrid/names.py +++ /dev/null @@ -1,185 +0,0 @@ -# stdlib -from secrets import randbelow - -left_name = [ - "admiring", - "adoring", - "affectionate", - "agitated", - "amazing", - "angry", - "awesome", - "beautiful", - "blissful", - "bold", - "boring", - "brave", - "busy", - "charming", - "clever", - "cool", - "compassionate", - "competent", - "condescending", - "confident", - "cranky", - "crazy", - "dazzling", - "determined", - "distracted", - "dreamy", - "eager", - "ecstatic", - "elastic", - "elated", - "elegant", - "eloquent", - "epic", - "exciting", - "fervent", - "festive", - "flamboyant", - "focused", - "friendly", - "frosty", - "funny", - "gallant", - "gifted", - "goofy", - "gracious", - "great", - "happy", - "hardcore", - "heuristic", - "hopeful", - "hungry", - "infallible", - "inspiring", - "interesting", - "intelligent", - "jolly", - "jovial", - "keen", - "kellis", - "kind", - "laughing", - "loving", - "lucid", - "magical", - "mystifying", - "modest", - "musing", - "naughty", - "nervous", - "nice", - "nifty", - "nostalgic", - "objective", - "optimistic", - "peaceful", - "pedantic", - "pensive", - "practical", - "priceless", - "quirky", - "quizzical", - "recursing", - "relaxed", - "reverent", - "romantic", - "sad", - "serene", - "sharp", - "silly", - "sleepy", - "stoic", - "strange", - "stupefied", - "suspicious", - "sweet", - "tender", - "thirsty", - "trusting", - "unruffled", - "upbeat", - "vibrant", - "vigilant", - "vigorous", - "wizardly", - "wonderful", - "xenodochial", - "youthful", - "zealous", - "zen", -] - -right_name = [ - "altman", - "bach", - "bengios", - "bostrom", - "botvinick", - "brockman", - "chintala", - "chollet", - "chomsky", - "dean", - "dolgov", - "eckersley", - "fridman", - "gardner", - "goertzel", - "goodfellow", - "hassabis", - "he", - "hinton", - "hochreiter", - "hotz", - "howard", - "hutter", - "isbell", - "kaliouby", - "karp", - "karpathy", - "kearns", - "kellis", - "knuth", - "koller", - "krizhevsky", - "larochelle", - "lattner", - "lecun", - "li", - "lim", - "littman", - "malik", - "mironov", - "ng", - "norvig", - "olah", - "pearl", - "pesenti", - "russell", - "salakhutdinov", - "schmidhuber", - "silver", - "smola", - "song", - "sophia", - "sutskever", - "thomas", - "thrun", - "trask", - "vapnik", - "vaswani", - "vinyals", - "winston", - "wolf", - "wolfram", -] - - -def random_name() -> str: - left_i = randbelow(len(left_name) - 1) - right_i = randbelow(len(right_name) - 1) - return f"{left_name[left_i].capitalize()} {right_name[right_i].capitalize()}" diff --git a/packages/hagrid/hagrid/nb_output.py b/packages/hagrid/hagrid/nb_output.py deleted file mode 100644 index c0a4bb0d2fb..00000000000 --- a/packages/hagrid/hagrid/nb_output.py +++ /dev/null @@ -1,15 +0,0 @@ -# future -from __future__ import annotations - - -# alert-info, alert-warning, alert-success, alert-danger -class NBOutput: - def __init__(self, raw_output: str) -> None: - self.raw_output = raw_output - - def _repr_html_(self) -> str: - return self.raw_output - - def to_html(self) -> NBOutput: - self.raw_output = self.raw_output.replace("\n", "
") - return self diff --git a/packages/hagrid/hagrid/orchestra.py b/packages/hagrid/hagrid/orchestra.py deleted file mode 100644 index 8826c073841..00000000000 --- a/packages/hagrid/hagrid/orchestra.py +++ /dev/null @@ -1,633 +0,0 @@ -"""Python Level API to launch Docker Containers using Hagrid""" - -# future -from __future__ import annotations - -# stdlib -from collections.abc import Callable -from enum import Enum -import getpass -import inspect -import os -import subprocess # nosec -import sys -from threading import Thread -from typing import Any -from typing import TYPE_CHECKING - -# relative -from .cli import str_to_bool -from .grammar import find_available_port -from .names import random_name -from .util import ImportFromSyft -from .util import NodeSideType -from .util import shell - -DEFAULT_PORT = 8080 -DEFAULT_URL = "http://localhost" -# Gevent used instead of threading module ,as we monkey patch gevent in syft -# and this causes context switch error when we use normal threading in hagrid - -ClientAlias = Any # we don't want to import Client in case it changes - -if TYPE_CHECKING: - NodeType = ImportFromSyft.import_node_type() - - -# Define a function to read and print a stream -def read_stream(stream: subprocess.PIPE) -> None: - while True: - line = stream.readline() - if not line: - break - print(line, end="") - - -def to_snake_case(name: str) -> str: - return name.lower().replace(" ", "_") - - -def get_syft_client() -> Any | None: - try: - # syft absolute - import syft as sy - - return sy - except Exception: # nosec - # print("Please install syft with `pip install syft`") - pass - return None - - -def container_exists(name: str) -> bool: - output = shell(f"docker ps -q -f name='{name}'") - return len(output) > 0 - - -def port_from_container(name: str, deployment_type: DeploymentType) -> int | None: - container_suffix = "" - if deployment_type == DeploymentType.SINGLE_CONTAINER: - container_suffix = "-worker-1" - elif deployment_type == DeploymentType.CONTAINER_STACK: - container_suffix = "-proxy-1" - else: - raise NotImplementedError( - f"port_from_container not implemented for the deployment type:{deployment_type}" - ) - - container_name = name + container_suffix - output = shell(f"docker port {container_name}") - if len(output) > 0: - try: - # 80/tcp -> 0.0.0.0:8080 - lines = output.split("\n") - parts = lines[0].split(":") - port = int(parts[1].strip()) - return port - except Exception: # nosec - return None - return None - - -def container_exists_with(name: str, port: int) -> bool: - output = shell( - f"docker ps -q -f name={name} | xargs -n 1 docker port | grep 0.0.0.0:{port}" - ) - return len(output) > 0 - - -def get_node_type(node_type: str | NodeType | None) -> NodeType | None: - NodeType = ImportFromSyft.import_node_type() - if node_type is None: - node_type = os.environ.get("ORCHESTRA_NODE_TYPE", NodeType.DOMAIN) - try: - return NodeType(node_type) - except ValueError: - print(f"node_type: {node_type} is not a valid NodeType: {NodeType}") - return None - - -def get_deployment_type(deployment_type: str | None) -> DeploymentType | None: - if deployment_type is None: - deployment_type = os.environ.get( - "ORCHESTRA_DEPLOYMENT_TYPE", DeploymentType.PYTHON - ) - - # provide shorthands - if deployment_type == "container": - deployment_type = "container_stack" - - try: - return DeploymentType(deployment_type) - except ValueError: - print( - f"deployment_type: {deployment_type} is not a valid DeploymentType: {DeploymentType}" - ) - return None - - -# Can also be specified by the environment variable -# ORCHESTRA_DEPLOYMENT_TYPE -class DeploymentType(Enum): - PYTHON = "python" - SINGLE_CONTAINER = "single_container" - CONTAINER_STACK = "container_stack" - K8S = "k8s" - PODMAN = "podman" - - -class NodeHandle: - def __init__( - self, - node_type: NodeType, - deployment_type: DeploymentType, - node_side_type: NodeSideType, - name: str, - port: int | None = None, - url: str | None = None, - python_node: Any | None = None, - shutdown: Callable | None = None, - ) -> None: - self.node_type = node_type - self.name = name - self.port = port - self.url = url - self.python_node = python_node - self.shutdown = shutdown - self.deployment_type = deployment_type - self.node_side_type = node_side_type - - @property - def client(self) -> Any: - if self.port: - sy = get_syft_client() - return sy.login_as_guest(url=self.url, port=self.port) # type: ignore - elif self.deployment_type == DeploymentType.PYTHON: - return self.python_node.get_guest_client(verbose=False) # type: ignore - else: - raise NotImplementedError( - f"client not implemented for the deployment type:{self.deployment_type}" - ) - - def login_as_guest(self, **kwargs: Any) -> ClientAlias: - return self.client.login_as_guest(**kwargs) - - def login( - self, email: str | None = None, password: str | None = None, **kwargs: Any - ) -> ClientAlias: - if not email: - email = input("Email: ") - - if not password: - password = getpass.getpass("Password: ") - - return self.client.login(email=email, password=password, **kwargs) - - def register( - self, - name: str, - email: str | None = None, - password: str | None = None, - password_verify: str | None = None, - institution: str | None = None, - website: str | None = None, - ) -> Any: - SyftError = ImportFromSyft.import_syft_error() - if not email: - email = input("Email: ") - if not password: - password = getpass.getpass("Password: ") - if not password_verify: - password_verify = getpass.getpass("Confirm Password: ") - if password != password_verify: - return SyftError(message="Passwords do not match") - - client = self.client - return client.register( - name=name, - email=email, - password=password, - institution=institution, - password_verify=password_verify, - website=website, - ) - - def land(self) -> None: - if self.deployment_type == DeploymentType.PYTHON: - if self.shutdown: - self.shutdown() - else: - Orchestra.land(self.name, deployment_type=self.deployment_type) - - -def deploy_to_python( - node_type_enum: NodeType, - deployment_type_enum: DeploymentType, - port: int | str, - name: str, - host: str, - reset: bool, - tail: bool, - dev_mode: bool, - processes: int, - local_db: bool, - node_side_type: NodeSideType, - enable_warnings: bool, - n_consumers: int, - thread_workers: bool, - create_producer: bool = False, - queue_port: int | None = None, - association_request_auto_approval: bool = False, - background_tasks: bool = False, -) -> NodeHandle | None: - stage_protocol_changes = ImportFromSyft.import_stage_protocol_changes() - NodeType = ImportFromSyft.import_node_type() - sy = get_syft_client() - if sy is None: - return sy - worker_classes = {NodeType.DOMAIN: sy.Domain, NodeType.NETWORK: sy.Gateway} - - # syft >= 0.8.2 - if hasattr(sy, "Enclave"): - worker_classes[NodeType.ENCLAVE] = sy.Enclave - if hasattr(NodeType, "GATEWAY"): - worker_classes[NodeType.GATEWAY] = sy.Gateway - - if dev_mode: - print("Staging Protocol Changes...") - stage_protocol_changes() - - kwargs = { - "name": name, - "host": host, - "port": port, - "reset": reset, - "processes": processes, - "dev_mode": dev_mode, - "tail": tail, - "node_type": node_type_enum, - "node_side_type": node_side_type, - "enable_warnings": enable_warnings, - # new kwargs - "queue_port": queue_port, - "n_consumers": n_consumers, - "create_producer": create_producer, - "association_request_auto_approval": association_request_auto_approval, - "background_tasks": background_tasks, - } - - if port: - kwargs["in_memory_workers"] = True - if port == "auto": - # dont use default port to prevent port clashes in CI - port = find_available_port(host="localhost", port=None, search=True) - kwargs["port"] = port - - sig = inspect.signature(sy.serve_node) - supported_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} - - start, stop = sy.serve_node(**supported_kwargs) - start() - return NodeHandle( - node_type=node_type_enum, - deployment_type=deployment_type_enum, - name=name, - port=port, - url="http://localhost", - shutdown=stop, - node_side_type=node_side_type, - ) - else: - kwargs["local_db"] = local_db - kwargs["thread_workers"] = thread_workers - if node_type_enum in worker_classes: - worker_class = worker_classes[node_type_enum] - sig = inspect.signature(worker_class.named) - supported_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} - if "node_type" in sig.parameters.keys() and "migrate" in sig.parameters: - supported_kwargs["migrate"] = True - worker = worker_class.named(**supported_kwargs) - else: - raise NotImplementedError(f"node_type: {node_type_enum} is not supported") - - def stop() -> None: - worker.stop() - - return NodeHandle( - node_type=node_type_enum, - deployment_type=deployment_type_enum, - name=name, - python_node=worker, - node_side_type=node_side_type, - shutdown=stop, - ) - - -def deploy_to_k8s( - node_type_enum: NodeType, - deployment_type_enum: DeploymentType, - name: str, - node_side_type: NodeSideType, -) -> NodeHandle: - node_port = int(os.environ.get("NODE_PORT", f"{DEFAULT_PORT}")) - node_url = str(os.environ.get("NODE_URL", f"{DEFAULT_URL}")) - return NodeHandle( - node_type=node_type_enum, - deployment_type=deployment_type_enum, - name=name, - port=node_port, - url=node_url, - node_side_type=node_side_type, - ) - - -def deploy_to_podman( - node_type_enum: NodeType, - deployment_type_enum: DeploymentType, - name: str, - node_side_type: NodeSideType, -) -> NodeHandle: - node_port = int(os.environ.get("NODE_PORT", f"{DEFAULT_PORT}")) - return NodeHandle( - node_type=node_type_enum, - deployment_type=deployment_type_enum, - name=name, - port=node_port, - url="http://localhost", - node_side_type=node_side_type, - ) - - -def deploy_to_container( - node_type_enum: NodeType, - deployment_type_enum: DeploymentType, - node_side_type: NodeSideType, - reset: bool, - cmd: bool, - tail: bool, - verbose: bool, - tag: str, - render: bool, - dev_mode: bool, - port: int | str, - name: str, - enable_warnings: bool, - in_memory_workers: bool, - association_request_auto_approval: bool = False, -) -> NodeHandle | None: - if port == "auto" or port is None: - if container_exists(name=name): - port = port_from_container(name=name, deployment_type=deployment_type_enum) # type: ignore - else: - port = find_available_port(host="localhost", port=DEFAULT_PORT, search=True) - - # Currently by default we launch in dev mode - if reset: - Orchestra.reset(name, deployment_type_enum) - else: - if container_exists_with(name=name, port=port): - return NodeHandle( - node_type=node_type_enum, - deployment_type=deployment_type_enum, - name=name, - port=port, - url="http://localhost", - node_side_type=node_side_type, - ) - - # Start a subprocess and capture its output - commands = ["hagrid", "launch"] - - name = random_name() if not name else name - commands.extend([name, node_type_enum.value]) - - commands.append("to") - commands.append(f"docker:{port}") - - if dev_mode: - commands.append("--dev") - - if not enable_warnings: - commands.append("--no-warnings") - - if node_side_type.lower() == NodeSideType.LOW_SIDE.value.lower(): - commands.append("--low-side") - - if in_memory_workers: - commands.append("--in-mem-workers") - - # by default , we deploy as container stack - if deployment_type_enum == DeploymentType.SINGLE_CONTAINER: - commands.append("--deployment-type=single_container") - - if association_request_auto_approval: - commands.append("--enable-association-auto-approval") - - if cmd: - commands.append("--cmd") - - if tail: - commands.append("--tail") - - if verbose: - commands.append("--verbose") - - if tag: - commands.append(f"--tag={tag}") - - if render: - commands.append("--render") - - # needed for building containers - USER = os.environ.get("USER", getpass.getuser()) - env = os.environ.copy() - env["USER"] = USER - - process = subprocess.Popen( # nosec - commands, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env - ) - # Start threads to read and print the output and error streams - stdout_thread = Thread(target=read_stream, args=(process.stdout,)) - stderr_thread = Thread(target=read_stream, args=(process.stderr,)) - # todo, raise errors - stdout_thread.start() - stderr_thread.start() - stdout_thread.join() - stderr_thread.join() - - if not cmd: - return NodeHandle( - node_type=node_type_enum, - deployment_type=deployment_type_enum, - name=name, - port=port, - url="http://localhost", - node_side_type=node_side_type, - ) - return None - - -class Orchestra: - @staticmethod - def launch( - # node information and deployment - name: str | None = None, - node_type: str | NodeType | None = None, - deploy_to: str | None = None, - node_side_type: str | None = None, - # worker related inputs - port: int | str | None = None, - processes: int = 1, # temporary work around for jax in subprocess - local_db: bool = False, - dev_mode: bool = False, - cmd: bool = False, - reset: bool = False, - tail: bool = False, - host: str | None = "0.0.0.0", # nosec - tag: str | None = "latest", - verbose: bool = False, - render: bool = False, - enable_warnings: bool = False, - n_consumers: int = 0, - thread_workers: bool = False, - create_producer: bool = False, - queue_port: int | None = None, - in_memory_workers: bool = True, - association_request_auto_approval: bool = False, - background_tasks: bool = False, - ) -> NodeHandle | None: - NodeType = ImportFromSyft.import_node_type() - os.environ["DEV_MODE"] = str(dev_mode) - if dev_mode is True: - thread_workers = True - - # syft 0.8.1 - if node_type == "python": - node_type = NodeType.DOMAIN - if deploy_to is None: - deploy_to = "python" - - dev_mode = str_to_bool(os.environ.get("DEV_MODE", f"{dev_mode}")) - - node_type_enum: NodeType | None = get_node_type(node_type=node_type) - - node_side_type_enum = ( - NodeSideType.HIGH_SIDE - if node_side_type is None - else NodeSideType(node_side_type) - ) - - deployment_type_enum: DeploymentType | None = get_deployment_type( - deployment_type=deploy_to - ) - if not deployment_type_enum: - return None - - if deployment_type_enum == DeploymentType.PYTHON: - return deploy_to_python( - node_type_enum=node_type_enum, - deployment_type_enum=deployment_type_enum, - port=port, - name=name, - host=host, - reset=reset, - tail=tail, - dev_mode=dev_mode, - processes=processes, - local_db=local_db, - node_side_type=node_side_type_enum, - enable_warnings=enable_warnings, - n_consumers=n_consumers, - thread_workers=thread_workers, - create_producer=create_producer, - queue_port=queue_port, - association_request_auto_approval=association_request_auto_approval, - background_tasks=background_tasks, - ) - - elif deployment_type_enum == DeploymentType.K8S: - return deploy_to_k8s( - node_type_enum=node_type_enum, - deployment_type_enum=deployment_type_enum, - name=name, - node_side_type=node_side_type_enum, - ) - - elif ( - deployment_type_enum == DeploymentType.CONTAINER_STACK - or deployment_type_enum == DeploymentType.SINGLE_CONTAINER - ): - return deploy_to_container( - node_type_enum=node_type_enum, - deployment_type_enum=deployment_type_enum, - reset=reset, - cmd=cmd, - tail=tail, - verbose=verbose, - tag=tag, - render=render, - dev_mode=dev_mode, - port=port, - name=name, - node_side_type=node_side_type_enum, - enable_warnings=enable_warnings, - in_memory_workers=in_memory_workers, - association_request_auto_approval=association_request_auto_approval, - ) - elif deployment_type_enum == DeploymentType.PODMAN: - return deploy_to_podman( - node_type_enum=node_type_enum, - deployment_type_enum=deployment_type_enum, - name=name, - node_side_type=node_side_type_enum, - ) - # else: - # print(f"deployment_type: {deployment_type_enum} is not supported") - # return None - - @staticmethod - def land( - name: str, deployment_type: str | DeploymentType, reset: bool = False - ) -> None: - deployment_type_enum = DeploymentType(deployment_type) - Orchestra.shutdown(name=name, deployment_type_enum=deployment_type_enum) - if reset: - Orchestra.reset(name, deployment_type_enum=deployment_type_enum) - - @staticmethod - def shutdown( - name: str, deployment_type_enum: DeploymentType, reset: bool = False - ) -> None: - if deployment_type_enum != DeploymentType.PYTHON: - snake_name = to_snake_case(name) - - if reset: - land_output = shell(f"hagrid land {snake_name} --force --prune-vol") - else: - land_output = shell(f"hagrid land {snake_name} --force") - if "Removed" in land_output: - print(f" βœ… {snake_name} Container Removed") - elif "No resource found to remove for project" in land_output: - print(f" βœ… {snake_name} Container does not exist") - else: - print( - f"❌ Unable to remove container: {snake_name} :{land_output}", - file=sys.stderr, - ) - - @staticmethod - def reset(name: str, deployment_type_enum: DeploymentType) -> None: - if deployment_type_enum == DeploymentType.PYTHON: - sy = get_syft_client() - _ = sy.Worker.named(name=name, processes=1, reset=True) # type: ignore - elif ( - deployment_type_enum == DeploymentType.CONTAINER_STACK - or deployment_type_enum == DeploymentType.SINGLE_CONTAINER - ): - Orchestra.shutdown( - name=name, deployment_type_enum=deployment_type_enum, reset=True - ) - else: - raise NotImplementedError( - f"Reset not implemented for the deployment type:{deployment_type_enum}" - ) diff --git a/packages/hagrid/hagrid/parse_template.py b/packages/hagrid/hagrid/parse_template.py deleted file mode 100644 index faa2c143ad6..00000000000 --- a/packages/hagrid/hagrid/parse_template.py +++ /dev/null @@ -1,327 +0,0 @@ -# stdlib -import hashlib -import os -import shutil -from urllib.parse import urlparse - -# third party -from jinja2 import Environment -from jinja2 import FileSystemLoader -from jinja2 import Template -import requests -from rich.progress import track -import yaml - -# relative -from .cache import DEFAULT_REPO -from .cache import STABLE_BRANCH -from .lib import hagrid_cache_dir -from .lib import manifest_template_path -from .lib import repo_src_path -from .mode import EDITABLE_MODE - -HAGRID_TEMPLATE_PATH = str(manifest_template_path()) - - -def read_yml_file(filename: str) -> tuple[dict | None, str]: - template = None - - with open(filename) as fp: - try: - text = fp.read() - template = yaml.safe_load(text) - template_hash = hashlib.sha256(text.encode("utf-8")).hexdigest() - except yaml.YAMLError as exc: - raise exc - - return template, template_hash - - -def read_yml_url(yml_url: str) -> tuple[dict | None, str]: - template = None - - try: - # download file - response = requests.get(yml_url) # nosec - if response.status_code != 200: - raise Exception(f"Failed to download: {yml_url}") - - # Save file to the local destination - try: - template = yaml.safe_load(response.content) - template_hash = hashlib.sha256(response.content).hexdigest() - except yaml.YAMLError as exc: - raise exc - - except Exception as e: - raise e - - return template, template_hash - - -def git_url_for_file(file_path: str, base_url: str, hash: str) -> str: - # url must have unix style slashes - return os.path.join(base_url, hash, file_path).replace(os.sep, "/") - - -def get_local_abs_path(target_dir: str, file_path: str) -> str: - local_path = os.path.join(target_dir, file_path) - return os.path.expanduser(local_path) - - -def is_url(string: str) -> bool: - try: - result = urlparse(string) - return all([result.scheme, result.netloc]) - except ValueError: - return False - - -def is_path(string: str) -> bool: - return os.path.exists(string) - - -def manifest_cache_path(template_hash: str) -> str: - return f"{hagrid_cache_dir()}/manifests/{template_hash}" - - -def url_from_repo(template_location: str | None) -> str | None: - if template_location is None: - return None - - if ":" in template_location and "/" in template_location: - parts = template_location.split(":") - branch_or_hash = parts[1] - repo = parts[0] - elif ":" not in template_location and "/" in template_location: - branch_or_hash = STABLE_BRANCH - repo = template_location - else: - branch_or_hash = template_location - repo = DEFAULT_REPO - - manifest_url = ( - f"https://raw.githubusercontent.com/{repo}/{branch_or_hash}" - "/packages/hagrid/hagrid/manifest_template.yml" - ) - - if is_url(manifest_url): - return manifest_url - return None - - -def get_template_yml(template_location: str | None) -> tuple[dict | None, str]: - if template_location: - if is_url(template_location): - template, template_hash = read_yml_url(template_location) - elif is_path(template_location): - template, template_hash = read_yml_file(template_location) - elif url_from_repo(template_location): - template, template_hash = read_yml_url(url_from_repo(template_location)) - else: - raise Exception(f"{template_location} is not valid") - else: - template_location = HAGRID_TEMPLATE_PATH - - template, template_hash = read_yml_file(template_location) - - if EDITABLE_MODE and is_path(template_location): - # save it to the same folder for dev mode - template_hash = "dev" - return template, template_hash - - -def setup_from_manifest_template( - host_type: str, - deployment_type: str, - template_location: str | None = None, - overwrite: bool = False, - verbose: bool = False, -) -> dict: - template, template_hash = get_template_yml(template_location) - - kwargs_to_parse = {} - - if template is None: - raise ValueError( - f"Failed to read {template_location}. Please check the file name or path is correct." - ) - - git_hash = template["hash"] - git_base_url = template["baseUrl"] - target_dir = manifest_cache_path(template_hash) - all_template_files = template["files"] - docker_tag = template["dockerTag"] - files_to_download = [] - - for package_name in all_template_files: - # Get all files w.r.t that package e.g. grid, syft, hagrid - template_files = all_template_files[package_name] - package_path = template_files["path"] - - # common files - files_to_download += [ - os.path.join(package_path, f) for f in template_files["common"] - ] - - # docker related files - if host_type in ["docker"]: - files_to_download += [ - os.path.join(package_path, f) for f in template_files["docker"] - ] - - # add k8s related files - # elif host_type in ["k8s"]: - # files_to_download += template_files["k8s"] - - else: - raise Exception(f"Hagrid template does not currently support {host_type}.") - - if EDITABLE_MODE and is_path(template_location): - # to test things in editable mode we can pass in a .yml file path and it will - # copy the files instead of download them - for src_file_path in track(files_to_download, description="Copying files"): - full_src_dir = f"{repo_src_path()}/{src_file_path}" - full_target_path = f"{target_dir}/{src_file_path}" - full_target_dir = os.path.dirname(full_target_path) - os.makedirs(full_target_dir, exist_ok=True) - - shutil.copyfile( - full_src_dir, - full_target_path, - ) - else: - download_files( - files_to_download=files_to_download, - git_hash=git_hash, - git_base_url=git_base_url, - target_dir=target_dir, - overwrite=overwrite, - verbose=verbose, - ) - - kwargs_to_parse["tag"] = docker_tag - return kwargs_to_parse - - -def deployment_dir(node_name: str) -> str: - return f"{hagrid_cache_dir()}/deployments/{node_name}" - - -def download_files( - files_to_download: list[str], - git_hash: str, - git_base_url: str, - target_dir: str, - overwrite: bool = False, - verbose: bool = False, -) -> None: - for src_file_path in track(files_to_download, description="Downloading files"): - # For now target file path is same as source file path - trg_file_path = src_file_path - local_destination = get_local_abs_path(target_dir, trg_file_path) - link_to_file = git_url_for_file(src_file_path, git_base_url, git_hash) - download_file( - link_to_file=link_to_file, - local_destination=local_destination, - overwrite=overwrite, - verbose=verbose, - ) - - -def render_templates( - node_name: str, - deployment_type: str, - template_location: str | None, - env_vars: dict, - host_type: str, -) -> None: - template, template_hash = get_template_yml(template_location) - - if template is None: - raise ValueError("Failed to read hagrid template.") - - src_dir = manifest_cache_path(template_hash) - target_dir = deployment_dir(node_name) - all_template_files = template["files"] - - jinja_template = JinjaTemplate(src_dir) - - files_to_render = [] - for package_name in all_template_files: - template_files = all_template_files[package_name] - - # Aggregate all the files to be rendered - - # common files - files_to_render += template_files["common"] - - if host_type in ["docker"]: - # docker related files - for template_file in template_files["docker"]: - if "default.env" not in template_file: - files_to_render.append(template_file) - - # Render the files - for file_path in files_to_render: - folder_path = template_files["path"] - # relative to src_dir - src_file_path = f"{folder_path}{file_path}" - target_file_path = f"{target_dir}/{file_path}" - os.makedirs(os.path.dirname(target_file_path), exist_ok=True) - jinja_template.substitute_vars(src_file_path, env_vars, target_file_path) - - -class JinjaTemplate: - def __init__(self, template_dir: str | os.PathLike) -> None: - self.directory = os.path.expanduser(template_dir) - self.environ = Environment( - loader=FileSystemLoader(self.directory), autoescape=True - ) - - def read_template_from_path(self, filepath: str) -> Template: - return self.environ.get_template(name=filepath) - - def substitute_vars( - self, template_path: str, vars_to_substitute: dict, target_path: str - ) -> None: - template = self.read_template_from_path(template_path) - rendered_template = template.render(vars_to_substitute) - self.save_to(rendered_template, target_path) - - def save_to(self, message: str, filename: str) -> None: - base_dir = self.directory - filepath = os.path.abspath(os.path.join(base_dir, filename)) - - # Create sub directories if does not exist - os.makedirs(os.path.dirname(filepath), exist_ok=True) - - # Save template to filepath - with open(filepath, "w") as fp: - fp.write(message) - - -def download_file( - link_to_file: str, - local_destination: str, - overwrite: bool = False, - verbose: bool = False, -) -> None: - file_dir = os.path.dirname(local_destination) - os.makedirs(file_dir, exist_ok=True) - - if not os.path.exists(local_destination) or overwrite: - try: - # download file - response = requests.get(link_to_file) # nosec - if response.status_code != 200: - raise Exception(f"Failed to download: {link_to_file}") - - # Save file to the local destination - open(local_destination, "wb").write(response.content) - - except Exception as e: - raise e - else: - if verbose: - print(f"Skipping download: {link_to_file} exists.") diff --git a/packages/hagrid/hagrid/quickstart_ui.py b/packages/hagrid/hagrid/quickstart_ui.py deleted file mode 100644 index 9d1f8fc2652..00000000000 --- a/packages/hagrid/hagrid/quickstart_ui.py +++ /dev/null @@ -1,356 +0,0 @@ -# stdlib -from dataclasses import dataclass -import os -from pathlib import Path -import sys -from urllib.parse import urlparse -import zipfile - -# third party -import click -import requests -from tqdm import tqdm - -# relative -from .cache import DEFAULT_BRANCH -from .cache import DEFAULT_REPO -from .cache import arg_cache -from .nb_output import NBOutput - -directory = os.path.expanduser("~/.hagrid/quickstart/") - - -def quickstart_download_notebook( - url: str, directory: str, reset: bool = False, overwrite_all: bool = False -) -> tuple[str, bool, bool]: - os.makedirs(directory, exist_ok=True) - file_name = os.path.basename(url).replace("%20", "_").replace(" ", "_") - file_path = directory + os.sep + file_name - file_path = os.path.abspath(file_path) - - file_exists = os.path.isfile(file_path) - if overwrite_all: - reset = True - - if file_exists and not reset: - response = click.prompt( - f"\nOverwrite {file_name}?", - prompt_suffix="(a/y/N)", - default="n", - show_default=False, - ) - if response.lower() == "a": - reset = True - overwrite_all = True - elif response.lower() == "y": - reset = True - else: - print(f"Skipping {file_name}") - reset = False - - downloaded = False - if not file_exists or file_exists and reset: - print(f"Downloading notebook: {file_name}") - r = requests.get(url, allow_redirects=True) # nosec - with open(os.path.expanduser(file_path), "wb") as f: - f.write(r.content) - downloaded = True - return file_path, downloaded, overwrite_all - - -def fetch_notebooks_for_url( - url: str, - directory: str, - reset: bool = False, - repo: str = DEFAULT_REPO, - branch: str = DEFAULT_BRANCH, - commit: str | None = None, -) -> list[str]: - downloaded_files = [] - allowed_schemes_as_url = ["http", "https"] - url_scheme = urlparse(url).scheme - # relative mode - if url_scheme not in allowed_schemes_as_url: - notebooks = get_urls_from_dir(repo=repo, branch=branch, commit=commit, url=url) - if url.endswith(".ipynb"): - file_name = os.path.basename(url) - url_parts = url.split("notebooks") - if len(url_parts) > 1: - url_dir = url_parts[-1] - else: - url_dir = url - url_dir = url_dir.replace(file_name, "") - else: - url_dir = url - notebook_files = [] - existing_count = 0 - for notebook_url in notebooks: - url_filename = os.path.basename(notebook_url) - url_dirname = os.path.dirname(notebook_url) - if ( - url_dirname.endswith(url_dir) - and os.path.isdir(directory + url_dir) - and os.path.isfile(directory + url_dir + os.sep + url_filename) - ): - notebook_files.append(url_dir + os.sep + url_filename) - existing_count += 1 - - if existing_count > 0: - plural = "s" if existing_count > 1 else "" - print( - f"You have {existing_count} existing notebook{plural} matching: {url}" - ) - for nb in notebook_files: - print(nb) - - overwrite_all = False - for notebook_url in tqdm(notebooks): - file_path, _, overwrite_all = quickstart_download_notebook( - url=notebook_url, - directory=os.path.abspath(directory + os.sep + str(url_dir) + os.sep), - reset=reset, - overwrite_all=overwrite_all, - ) - downloaded_files.append(file_path) - - else: - file_path, _, _ = quickstart_download_notebook( - url=url, directory=directory, reset=reset - ) - downloaded_files.append(file_path) - return downloaded_files - - -def quickstart_extract_notebook( - zip_file: str, - name: str, - directory: Path, - reset: bool = False, - overwrite_all: bool = False, -) -> tuple[str, bool, bool]: - directory.mkdir(exist_ok=True) - reset = overwrite_all - - base_name = os.path.basename(name) - file_path = directory / base_name - file_name = file_path.name - file_exists = file_path.exists() - - if file_exists and not reset: - response = click.prompt( - f"\nOverwrite {file_name}?", - prompt_suffix="(a/y/N)", - default="n", - show_default=False, - ) - if response.lower() == "a": - reset = True - overwrite_all = True - elif response.lower() == "y": - reset = True - else: - print(f"Skipping {file_name}") - reset = False - - extracted = False - if not file_exists or file_exists and reset: - print(f"Extracting notebook: {file_name}") - with zipfile.ZipFile(zip_file, "r") as zf: - zip_info = zf.getinfo(name) - zip_info.filename = base_name - zf.extract(zip_info, directory) - extracted = True - return str(file_path.absolute()), extracted, overwrite_all - - -def fetch_notebooks_from_zipfile( - path: str, directory: str, reset: bool = False -) -> list[str]: - dir_path = Path(directory) - - with zipfile.ZipFile(path, "r") as zf: - notebooks = [f for f in zf.namelist() if f.endswith(".ipynb")] - - notebook_files = [dir_path / os.path.basename(nb) for nb in notebooks] - existing_files = [nb for nb in notebook_files if nb.exists()] - - existing_count = len(existing_files) - - if existing_count > 0: - plural = "s" if existing_count > 1 else "" - print(f"You have {existing_count} existing notebook{plural}") - for nb in existing_files: - print(nb) - - extracted_files = [] - overwrite_all = False - for notebook in tqdm(notebooks): - file_path, _, overwrite_all = quickstart_extract_notebook( - zip_file=path, - name=notebook, - directory=dir_path, - reset=reset, - overwrite_all=overwrite_all, - ) - extracted_files.append(file_path) - - return extracted_files - - -@dataclass -class Tutorial: - filename: str - description: str - url: str - - -REPO_RAW_PATH = "https://raw.githubusercontent.com/OpenMined/PySyft" - -TUTORIALS = { - "api/0.8": Tutorial( - filename="api/0.8", - description="0.8 API Notebooks", - url="api/0.8", - ), - "hello-syft": Tutorial( - filename="tutorials/hello-syft", - description="Hello Syft", - url="tutorials/hello-syft", - ), - "data-engineer": Tutorial( - filename="tutorials/data-engineer", - description="Data Engineer", - url="tutorials/data-engineer", - ), - "data-owner": Tutorial( - filename="tutorials/data-owner", - description="Data Owner", - url="tutorials/data-owner", - ), - "data-scientist": Tutorial( - filename="tutorials/data-scientist", - description="Data Scientist", - url="tutorials/data-scientist", - ), - "pandas-cookbook": Tutorial( - filename="tutorials/pandas-cookbook", - description="Pandas Cookbook", - url="tutorials/pandas-cookbook", - ), -} - - -class QuickstartUI: - @property - def tutorials(self) -> dict[str, Tutorial]: - return TUTORIALS - - def download( - self, tutorial_name: str, reset: bool = False, branch: str = "dev" - ) -> NBOutput: - if tutorial_name not in TUTORIALS.keys(): - return NBOutput( - f'
{tutorial_name} is not a valid tutorial name.
' - ) - else: - tutorial = TUTORIALS[tutorial_name] - downloaded_files = fetch_notebooks_for_url( - url=tutorial.url, directory=directory, branch=branch - ) - html = "" - if len(downloaded_files) == 0: - html += f'
{tutorial_name} failed to download.' - else: - first = downloaded_files[0] - jupyter_path = first.replace(os.path.abspath(directory) + "/", "") - - html += f'
{tutorial_name} downloaded.' - html += f'
πŸ“– Click to Open Tutorial
' - return NBOutput(html) - - def _repr_html_(self) -> str: - html = "" - if not arg_cache["install_wizard_complete"]: - html += "

Step 1b: Install πŸ§™πŸ½β€β™‚οΈ Wizard (Recommended)

" - html += ( - "It looks like this might be your first time running Quickstart.
" - ) - html += ( - "
Please go through the Install Wizard notebook to " - + "install Syft and optionally start a Grid server." - ) - html += ( - '
πŸ“– Click to start ' - + "Install πŸ§™πŸ½β€β™‚οΈ Wizard
" - ) - html += "
" - - html += "

Download Tutorials

" - html += "Below is a list of tutorials to download using quickstart.
" - html += "
    " - for name, tutorial in TUTORIALS.items(): - html += ( - "
  • πŸ“– Tutorial Series: " - + f"{name}
    {tutorial.description}
  • " - ) - html += "
" - first = list(TUTORIALS.keys())[0] - html += ( - "
Try running:
" - + f'quickstart.download("{first}")
' - ) - - return html - - -def get_urls_from_dir( - url: str, - repo: str, - branch: str, - commit: str | None = None, -) -> list[str]: - notebooks = [] - slug = commit if commit else branch - - gh_api_call = ( - "https://api.github.com/repos/" + repo + "/git/trees/" + slug + "?recursive=1" - ) - r = requests.get(gh_api_call) # nosec - if r.status_code != 200: - print( - f"Failed to fetch notebook from: {gh_api_call}.\n" - + "Please try again with the correct parameters!" - ) - sys.exit(1) - - res = r.json() - - for file in res["tree"]: - if file["path"].startswith("notebooks/quickstart/" + url) or file[ - "path" - ].startswith("notebooks/" + url): - if file["path"].endswith(".ipynb"): - temp_url = ( - "https://raw.githubusercontent.com/" - + repo - + "/" - + slug - + "/" - + file["path"] - ) - notebooks.append(temp_url) - - if len(notebooks) == 0: - for file in res["tree"]: - if file["path"].startswith("notebooks/" + url): - if file["path"].endswith(".ipynb"): - temp_url = ( - "https://raw.githubusercontent.com/" - + repo - + "/" - + slug - + "/" - + file["path"] - ) - notebooks.append(temp_url) - return notebooks diff --git a/packages/hagrid/hagrid/rand_sec.py b/packages/hagrid/hagrid/rand_sec.py deleted file mode 100644 index 3323554a72f..00000000000 --- a/packages/hagrid/hagrid/rand_sec.py +++ /dev/null @@ -1,84 +0,0 @@ -# stdlib -from os import urandom -import string -import sys - - -def generate_sec_random_password( - length: int, - special_chars: bool = True, - digits: bool = True, - lower_case: bool = True, - upper_case: bool = True, -) -> str: - """Generates a random password of the given length. - - Args: - length (int): length of the password - special_chars (bool, optional): Include at least one specials char in the password. Defaults to True. - digits (bool, optional): Include at least one digit in the password. Defaults to True. - lower_case (bool, optional): Include at least one lower case character in the password. Defaults to True. - upper_case (bool, optional): Includde at least one upper case character in the password. Defaults to True. - - Raises: - ValueError: If password length if too short. - - Returns: - str: randomly generated password - """ - if not isinstance(length, int) or length < 10: - raise ValueError( - "Password should have a positive safe length of at least 10 characters!" - ) - - choices: str = "" - required_tokens: list[str] = [] - if special_chars: - special_characters = "!@#$%^&*()_+" - choices += special_characters - required_tokens.append( - special_characters[ - int.from_bytes(urandom(1), sys.byteorder) % len(special_characters) - ] - ) - if lower_case: - choices += string.ascii_lowercase - required_tokens.append( - string.ascii_lowercase[ - int.from_bytes(urandom(1), sys.byteorder) % len(string.ascii_lowercase) - ] - ) - if upper_case: - choices += string.ascii_uppercase - required_tokens.append( - string.ascii_uppercase[ - int.from_bytes(urandom(1), sys.byteorder) % len(string.ascii_uppercase) - ] - ) - if digits: - choices += string.digits - required_tokens.append( - string.digits[ - int.from_bytes(urandom(1), sys.byteorder) % len(string.digits) - ] - ) - - # Python 3 (urandom returns bytes) - password = [choices[c % len(choices)] for c in urandom(length)] - - # Pick some random indexes - random_indexes: set[int] = set() - while len(random_indexes) < len(required_tokens): - random_indexes.add(int.from_bytes(urandom(1), sys.byteorder) % len(password)) - - # Replace the random indexes with the required tokens - for i, idx in enumerate(random_indexes): - password[idx] = required_tokens[i] - - return "".join(password) - - -if __name__ == "__main__": - pwd_length = 48 - # generate_sec_random_password(pwd_length) - print(generate_sec_random_password(pwd_length, special_chars=False)) diff --git a/packages/hagrid/hagrid/stable_version.py b/packages/hagrid/hagrid/stable_version.py deleted file mode 100644 index d596aae77cd..00000000000 --- a/packages/hagrid/hagrid/stable_version.py +++ /dev/null @@ -1 +0,0 @@ -LATEST_STABLE_SYFT = "0.8.6" diff --git a/packages/hagrid/hagrid/style.py b/packages/hagrid/hagrid/style.py deleted file mode 100644 index f42d9f79061..00000000000 --- a/packages/hagrid/hagrid/style.py +++ /dev/null @@ -1,44 +0,0 @@ -# stdlib -import io - -# third party -import click -import rich - -# relative -from .deps import DEPENDENCIES -from .mode import EDITABLE_MODE - - -class RichGroup(click.Group): - def format_usage( - self, ctx: click.core.Context, formatter: click.formatting.HelpFormatter - ) -> None: - sio = io.StringIO() - console = rich.get_console() - mode = "" - if EDITABLE_MODE: - mode = "[bold red]EDITABLE DEV MODE[/bold red] :police_car_light:" - console.print( - "[bold red]HA[/bold red][bold magenta]Grid[/bold magenta]!", ":mage:", mode - ) - table = rich.table.Table() - - table.add_column("Dependency", style="magenta") - table.add_column("Found", justify="right") - - for dep in sorted(DEPENDENCIES.keys()): - path = DEPENDENCIES[dep] - installed_str = ":white_check_mark:" if path is not None else ":cross_mark:" - dep_emoji = ":gear:" - if dep == "docker": - dep_emoji = ":whale:" - if dep == "git": - dep_emoji = ":file_folder:" - if dep == "ansible-playbook": - dep_emoji = ":blue_book:" - table.add_row(f"{dep_emoji} {dep}", installed_str) - # console.print(dep_emoji, dep, installed_str) - console.print(table) - console.print("Usage: hagrid [OPTIONS] COMMAND [ARGS]...") - formatter.write(sio.getvalue()) diff --git a/packages/hagrid/hagrid/util.py b/packages/hagrid/hagrid/util.py deleted file mode 100644 index 73d1cf1e34e..00000000000 --- a/packages/hagrid/hagrid/util.py +++ /dev/null @@ -1,102 +0,0 @@ -# stdlib -from collections.abc import Callable -from enum import Enum -import os -import subprocess # nosec -import sys -from typing import Any -from urllib.parse import urlparse - -# relative -from .dummynum import DummyNum - - -class NodeSideType(str, Enum): - LOW_SIDE = "low" - HIGH_SIDE = "high" - - def __str__(self) -> str: - # Use values when transforming NodeType to str - return self.value - - -class ImportFromSyft: - @staticmethod - def import_syft_error() -> Callable: - try: - # syft absolute - from syft.service.response import SyftError - except Exception: - SyftError = DummyNum - - return SyftError - - @staticmethod - def import_stage_protocol_changes() -> Callable: - try: - # syft absolute - from syft.protocol.data_protocol import stage_protocol_changes - except Exception: - - def stage_protocol_changes(*args: Any, **kwargs: Any) -> None: - pass - - return stage_protocol_changes - - @staticmethod - def import_node_type() -> Callable: - try: - # syft absolute - from syft.abstract_node import NodeType - except Exception: - NodeType = DummyNum - - return NodeType - - -def from_url(url: str) -> tuple[str, str, int, str, Any | str]: - try: - # urlparse doesnt handle no protocol properly - if "://" not in url: - url = "http://" + url - parts = urlparse(url) - host_or_ip_parts = parts.netloc.split(":") - # netloc is host:port - port = 80 - if len(host_or_ip_parts) > 1: - port = int(host_or_ip_parts[1]) - host_or_ip = host_or_ip_parts[0] - return ( - host_or_ip, - parts.path, - port, - parts.scheme, - getattr(parts, "query", ""), - ) - except Exception as e: - print(f"Failed to convert url: {url} to GridURL. {e}") - raise e - - -def fix_windows_virtualenv_api(cls: type) -> None: - # fix bug in windows - def _python_rpath(self: Any) -> str: - """The relative path (from environment root) to python.""" - # Windows virtualenv installation installs pip to the [Ss]cripts - # folder. Here's a simple check to support: - if sys.platform == "win32": - # fix here https://github.com/sjkingo/virtualenv-api/issues/47 - return os.path.join(self.path, "Scripts", "python.exe") - return os.path.join("bin", "python") - - cls._python_rpath = property(_python_rpath) - - -def shell(command: str) -> str: - try: - output = subprocess.check_output( # nosec - command, shell=True, stderr=subprocess.STDOUT - ) - except Exception: - output = b"" - return output.decode("utf-8") diff --git a/packages/hagrid/hagrid/version.py b/packages/hagrid/hagrid/version.py deleted file mode 100644 index 22a3553ca00..00000000000 --- a/packages/hagrid/hagrid/version.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 -# HAGrid Version -__version__ = "0.3.121" - -if __name__ == "__main__": - print(__version__) diff --git a/packages/hagrid/hagrid/win_bootstrap.py b/packages/hagrid/hagrid/win_bootstrap.py deleted file mode 100644 index 9cd79c24c36..00000000000 --- a/packages/hagrid/hagrid/win_bootstrap.py +++ /dev/null @@ -1,267 +0,0 @@ -# stdlib -from collections.abc import Callable -import subprocess # nosec - -# one liner to use bootstrap script: -# CMD: curl https://raw.githubusercontent.com/OpenMined/PySyft/dev/packages/hagrid/hagrid/win_bootstrap.py > win_bootstrap.py && python win_bootstrap.py # noqa -# Powershell is complaining about a utf-8 issue we need to fix, could be related to a -# bug with long lines in utf-8 -# PS: $r = Invoke-WebRequest "https://raw.githubusercontent.com/OpenMined/PySyft/dev/packages/hagrid/hagrid/win_bootstrap.py" -UseBasicParsing; echo $r.Content > win_bootstrap.py; python win_bootstrap.py # noqa - - -class Requirement: - def __init__( - self, full_name: str, choco_name: str, detect: Callable, extras: str = "" - ) -> None: - self.full_name = full_name - self.choco_name = choco_name - self.detect = detect - self.extras = extras - - def __repr__(self) -> str: - return self.full_name - - -install_choco_pwsh = """ -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; -Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')); -""" - -install_wsl2_pwsh = """ -wsl --update; wsl --shutdown; wsl --set-default-version 2; wsl --install -d Ubuntu; wsl --setdefault Ubuntu; -""" - -# add this to block powershell from existing for debugging -# Read-Host -Prompt string - - -def make_admin_cmd(admin_cmd: str) -> str: - return ( - f"Start-Process PowerShell -Wait -Verb RunAs -ArgumentList " - '"' - "Set-ExecutionPolicy Bypass -Scope Process -Force; " - f"{admin_cmd}; " - '"' - ) - - -def where_is(binary: str, req: Requirement) -> bool: - print(f"{req.full_name} - {binary}", end="", flush=True) - found = path_where_is(binary) - if not found: - found = full_where_is(binary) - if found: - print(" √") - else: - print(" Γ—") - return found - - -def path_where_is(binary: str) -> bool: - try: - cmds = ["where.exe", binary] - output = subprocess.run(cmds, capture_output=True, cwd="C:\\") # nosec - out = str(output.stdout.decode("utf-8")).split("\r\n") - if binary in out[0]: - return True - except Exception as e: - print("error", e) - pass - return False - - -def full_where_is(binary: str) -> bool: - try: - powershell_cmd = f"where.exe /R C:\ *.exe | findstr \\{binary}$" # noqa: W605 - cmds = ["powershell.exe", "-Command", powershell_cmd] - output = subprocess.run(cmds, capture_output=True, cwd="C:\\") # nosec - out = str(output.stdout.decode("utf-8")).split("\r\n") - if binary in out[0]: - return True - except Exception as e: - print("error", e) - pass - return False - - -def exe(binary: str) -> Callable: - def call(req: Requirement) -> bool: - return where_is(binary=binary, req=req) - - return call - - -def detect_wsl2(req: Requirement) -> bool: - print(f"{req.full_name} - wsl.exe ", end="") - try: - powershell_cmd = "wsl.exe --status" - cmds = ["powershell.exe", "-Command", powershell_cmd] - output = subprocess.run(cmds, capture_output=True) # nosec - out = output.stdout.decode("utf-16") - if "Default Distribution: Ubuntu" in out: - pass - if "Default Version: 2" in out: - print(" √") - return True - except Exception as e: - print("error", e) - pass - print(" Γ—") - return False - - -requirements = [] -requirements.append( - Requirement( - full_name="Windows Subsystem for Linux 2", - choco_name="wsl2", - detect=detect_wsl2, - ) -) -requirements.append( - Requirement( - full_name="Chocolatey Package Manager", - choco_name="choco", - detect=exe("choco.exe"), - ) -) -requirements.append( - Requirement( - full_name="Anaconda Individual Edition", - choco_name="anaconda3", - detect=exe("conda.exe"), - ) -) -requirements.append( - Requirement( - full_name="Git Version Control", - choco_name="git", - detect=exe("git.exe"), - ) -) -requirements.append( - Requirement( - full_name="Docker Desktop", - choco_name="docker-desktop", - detect=exe("docker.exe"), - ) -) - - -def install_elevated_powershell(full_name: str, powershell_cmd: str) -> None: - try: - input( - f"\nInstalling {full_name} requires Administrator.\n" - "When the UAC dialogue appears click Yes on the left.\n\n" - "Press enter to start..." - ) - powershell_cmds = ["-command", powershell_cmd] - output = subprocess.run( # nosec - ["powershell.exe"] + powershell_cmds, capture_output=True - ) - _ = output.stdout.decode("utf-8") - except Exception as e: - print("failed", e) - - -def install_choco() -> None: - return install_elevated_powershell( - full_name="Chocolatey", powershell_cmd=make_admin_cmd(install_choco_pwsh) - ) - - -def install_wsl2() -> None: - return install_elevated_powershell( - full_name="WSL2", powershell_cmd=make_admin_cmd(install_wsl2_pwsh) - ) - - -def install_deps(requirements: list[Requirement]) -> None: - package_names = [] - for req in requirements: - package_names.append(req.choco_name) - - try: - input( - "\nInstalling packages requires Administrator.\n" - "When the UAC dialogue appears click Yes on the left.\n\n" - "Press enter to start..." - ) - choco_args = f"choco.exe install {' '.join(package_names)} -y" - powershell_cmds = ["-command", make_admin_cmd(choco_args)] - output = subprocess.run( # nosec - ["powershell.exe"] + powershell_cmds, capture_output=True - ) - _ = str(output.stdout.decode("utf-8")) - except Exception as e: - print("failed", e) - - -def ask_install(requirement: Requirement) -> bool: - val = input(f"Do you want to install {requirement.full_name} (Y/n): ") - if "y" in val.lower(): - return True - return False - - -def check_all(requirements: list[Requirement]) -> list[Requirement]: - missing = [] - for req in requirements: - if not req.detect(req): - missing.append(req) - return missing - - -def main() -> None: - print("\nHAGrid Windows Dependency Installer") - print("===================================\n") - print("Searching your computer for:") - missing_deps = check_all(requirements=requirements) - - if len(missing_deps) > 0: - print("\nWe were unable to find the following dependencies:") - print("-----------------------------------") - for dep in missing_deps: - print(f"{dep.full_name}") - - print("") - desired = [] - choco_required = False - wsl2_required = False - for dep in missing_deps: - if ask_install(dep): - if dep.choco_name == "choco": - choco_required = True - elif dep.choco_name == "wsl2": - wsl2_required = True - else: - desired.append(dep) - elif dep.choco_name == "choco": - print("You must install Chocolatey to install other dependencies") - return - - if wsl2_required: - install_wsl2() - - if choco_required: - install_choco() - - if len(desired) > 0: - install_deps(desired) - - print("") - still_missing = check_all(requirements=missing_deps) - if len(still_missing) > 0: - print("We were still unable to find the following dependencies:") - print("-----------------------------------") - for dep in still_missing: - print(f"{dep.full_name}") - print("Please try again.") - else: - print("\nCongratulations. All done.") - print("===================================\n") - print("Now you can run HAGrid on Windows!") - - -if __name__ == "__main__": - main() diff --git a/packages/hagrid/hagrid/wizard_ui.py b/packages/hagrid/hagrid/wizard_ui.py deleted file mode 100644 index 7f4c5c1c0d4..00000000000 --- a/packages/hagrid/hagrid/wizard_ui.py +++ /dev/null @@ -1,62 +0,0 @@ -# stdlib - -# relative -from .cache import arg_cache -from .deps import Dependency -from .deps import check_grid_docker -from .deps import check_hagrid -from .deps import check_syft -from .deps import check_syft_deps -from .nb_output import NBOutput - -steps = {} -steps["check_hagrid"] = False -steps["check_syft"] = False -steps["check_grid"] = False - - -def complete_install_wizard( - output: dict[str, Dependency] | NBOutput, -) -> dict[str, Dependency] | NBOutput: - flipped = arg_cache["install_wizard_complete"] - if not flipped: - for _, v in steps.items(): - if v is False: - return output - arg_cache["install_wizard_complete"] = True - if isinstance(output, NBOutput): - if flipped != arg_cache["install_wizard_complete"]: - output.raw_output += "\n\nβœ… You have completed the Install Wizard" - return output - - -class WizardUI: - @property - def check_hagrid(self) -> dict[str, Dependency] | NBOutput: - steps["check_hagrid"] = True - return complete_install_wizard(check_hagrid()) - - @property - def check_syft_deps(self) -> dict[str, Dependency] | NBOutput: - steps["check_syft"] = True - return complete_install_wizard(check_syft_deps()) - - @property - def check_syft(self) -> dict[str, Dependency] | NBOutput: - steps["check_syft"] = True - return complete_install_wizard(check_syft()) - - @property - def check_syft_pre(self) -> dict[str, Dependency] | NBOutput: - steps["check_syft"] = True - return complete_install_wizard(check_syft(pre=True)) - - @property - def check_grid_docker(self) -> dict[str, Dependency] | NBOutput: - print("Deprecated. Please use .check_docker") - return self.check_docker - - @property - def check_docker(self) -> dict[str, Dependency] | NBOutput: - steps["check_grid"] = True - return complete_install_wizard(check_grid_docker()) diff --git a/packages/hagrid/scripts/install.sh b/packages/hagrid/scripts/install.sh deleted file mode 100755 index aa13f184093..00000000000 --- a/packages/hagrid/scripts/install.sh +++ /dev/null @@ -1,232 +0,0 @@ -#!/bin/sh - -# run with: -# curl https://raw.githubusercontent.com/OpenMined/PySyft/dev/packages/hagrid/scripts/install.sh | sh - -set -e - -cat /dev/null </dev/null - RETURN_CODE=$? - set +e - return $RETURN_CODE -} - -check_ubuntu() { - if [ -f /etc/os-release ] - then - . /etc/os-release - if [ "$ID" = "ubuntu" ] - then - return 0 - fi - fi - return 1 -} - -check_macos() { - if [ "$(uname)" = "Darwin" ] - then - return 0 - fi - return 1 -} - -check_os_supported() { - echo "Checking OS..." - if check_macos - then - echo "βœ… macOS detected" - return 0 - elif check_ubuntu - then - echo "βœ… Ubuntu detected" - return 0 - fi - echo $OS_NOT_SUPPORTED - exit 1 -} - -apt_install() { - execute_sudo "apt-get -qq -o=Dpkg::Use-Pty=0 update -y" - execute_sudo "apt-get -qq -o=Dpkg::Use-Pty=0 install $1 -y" -} - -brew_install() { - if is_command "brew" - then - # echo "Would run: brew install $1" - HOMEBREW_NO_AUTO_UPDATE=1 brew install $1 - else - echo "\nWe require brew to install packages.\nYou must install brew first: https://brew.sh/" - exit 1 - fi -} - -hagrid_install() { - echo "\nChecking hagrid ..." - if is_command "hagrid" - then - echo "βœ… hagrid detected" - else - echo "Installing hagrid" - pip install --quiet -U hagrid - fi -} - -# from: https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -unset HAVE_SUDO_ACCESS # unset this from the environment - -abort() { - printf "%s\n" "$@" >&2 - exit 1 -} - - -have_sudo_access() { - if [[ ! -x "/usr/bin/sudo" ]] - then - return 1 - fi - - local -a SUDO=("/usr/bin/sudo") - if [[ -n "${SUDO_ASKPASS-}" ]] - then - SUDO+=("-A") - elif [[ -n "${NONINTERACTIVE-}" ]] - then - SUDO+=("-n") - fi - - if [[ -z "${HAVE_SUDO_ACCESS-}" ]] - then - if [[ -n "${NONINTERACTIVE-}" ]] - then - "${SUDO[@]}" -l mkdir &>/dev/null - else - "${SUDO[@]}" -v && "${SUDO[@]}" -l mkdir &>/dev/null - fi - HAVE_SUDO_ACCESS="$?" - fi - - if [[ -n "${HOMEBREW_ON_MACOS-}" ]] && [[ "${HAVE_SUDO_ACCESS}" -ne 0 ]] - then - abort "Need sudo access on macOS (e.g. the user ${USER} needs to be an Administrator)!" - fi - - return "${HAVE_SUDO_ACCESS}" -} - -execute() { - if ! "$@" - then - abort "$(printf "Failed during: %s" "$(shell_join "$@")")" - fi -} - -execute_sudo() { - local -a args=("$@") - if have_sudo_access - then - if [[ -n "${SUDO_ASKPASS-}" ]] - then - args=("-A" "${args[@]}") - fi - echo "/usr/bin/sudo" "${args[@]}" - execute "/usr/bin/sudo" "${args[@]}" - else - echo "${args[@]}" - execute "${args[@]}" - fi -} - -check_and_install() { - echo "\nChecking $1 ..." - if is_command $1 - then - echo "βœ… $1 detected" - return 0 - else - echo "Installing missing dependency $2" - if check_macos - then - brew_install $2 - elif check_ubuntu - then - apt_install $2 - fi - fi - - if is_command $1 - then - echo "βœ… $1 detected" - return 0 - else - echo "Failed to install $1. Please manually install it." - fi -} - -check_install_python() { - check_and_install python3 python3 -} - -check_install_pip() { - echo "\nChecking pip ..." - if is_command "pip" - then - echo "βœ… pip detected" - return 0 - else - if check_macos - then - echo "Installing missing dependency pip" - python3 -m ensurepip - else - check_and_install pip python3-pip - fi - fi - - if is_command "pip" - then - echo "βœ… pip detected" - else - echo "Failed to install pip. Please manually install it." - fi -} - -check_install_git() { - check_and_install git git -} - -execute() { - check_os_supported - check_install_python - check_install_pip - check_install_git - - hagrid_install - - if is_command "hagrid" - then - echo "\nπŸ§™β€β™‚οΈ HAGrid is installed!\n" - echo "To get started run: \n$ hagrid quickstart\n" - else - echo "\nHAGrid failed to install. Please try manually with:" - echo "pip install -U hagrid" - exit 1 - fi -} - -execute diff --git a/packages/hagrid/scripts/update_manifest.py b/packages/hagrid/scripts/update_manifest.py deleted file mode 100644 index 4f31428c2ab..00000000000 --- a/packages/hagrid/scripts/update_manifest.py +++ /dev/null @@ -1,47 +0,0 @@ -# stdlib -import os -import subprocess -import sys - -# third party -import yaml - - -def latest_commit_id() -> str: - cmd = 'git log --format="%H" -n 1' - commit_id = subprocess.check_output(cmd, shell=True) - return commit_id.decode("utf-8").strip() - - -def update_manifest(docker_tag: str | None) -> None: - """Update manifest_template file with latest commit hash.""" - - # Get latest commit id - commit_id = latest_commit_id() - - template_dir = os.path.abspath(os.path.join(os.path.realpath(__file__), "../../")) - template_filepath = os.path.join(template_dir, "hagrid/manifest_template.yml") - - # open the manifest file - with open(template_filepath) as stream: - template_dict = yaml.safe_load(stream) - - # update commit id - template_dict["hash"] = commit_id - - # update docker tag if available - if docker_tag: - template_dict["dockerTag"] = docker_tag - - # save manifest file - with open(template_filepath, "w") as fp: - yaml.dump(template_dict, fp, sort_keys=False) - - -if __name__ == "__main__": - docker_tag = None - - if len(sys.argv) > 1: - docker_tag = sys.argv[1] - - update_manifest(docker_tag) # Update manifest file diff --git a/packages/hagrid/setup.py b/packages/hagrid/setup.py deleted file mode 100644 index 5dc9c72f5e4..00000000000 --- a/packages/hagrid/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -# stdlib -import platform - -# third party -from setuptools import find_packages -from setuptools import setup - -__version__ = "0.3.121" - -DATA_FILES = {"img": ["hagrid/img/*.png"], "hagrid": ["*.yml"]} - -packages = [ - "ascii_magic", - "click>=8.1.7", - "cryptography>=41.0.4", - "gitpython", - "jinja2", - "names", - "packaging>=23.0", - "paramiko", - "pyOpenSSL>=23.2.0", - "requests", - "rich", - "setuptools", - "virtualenv-api", - "virtualenv", - "PyYAML", - "tqdm", - "gevent>=22.10.2,<=23.9.1", -] - -if platform.system().lower() != "windows": - packages.extend(["ansible", "ansible-core"]) - -setup( - name="hagrid", - description="Happy Automation for Grid", - long_description="HAGrid is the swiss army knife of OpenMined's PySyft and PyGrid.", - long_description_content_type="text/plain", - version=__version__, - author="Andrew Trask ", - packages=find_packages(), - package_data=DATA_FILES, - install_requires=packages, - include_package_data=True, - entry_points={"console_scripts": ["hagrid = hagrid.cli:cli"]}, -) diff --git a/packages/hagrid/tests/__init__.py b/packages/hagrid/tests/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/hagrid/tests/hagrid/__init__.py b/packages/hagrid/tests/hagrid/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/hagrid/tests/hagrid/cli_test.py b/packages/hagrid/tests/hagrid/cli_test.py deleted file mode 100644 index 346988d527f..00000000000 --- a/packages/hagrid/tests/hagrid/cli_test.py +++ /dev/null @@ -1,199 +0,0 @@ -# stdlib -from collections import defaultdict - -# third party -from hagrid import cli -from hagrid import grammar - - -def test_hagrid_launch() -> None: - """This test is important because we want to make it convenient - for our users to just run 'hagrid launch' whenever they want to spin - up a new node with a randomly chosen name""" - - # COMMAND: "hagrid launch" - args: list[str] = [] - - verb = cli.get_launch_verb() - grammar = cli.parse_grammar(args=tuple(args), verb=verb) - verb.load_grammar(grammar=grammar) - cmd = cli.create_launch_cmd( - verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True - ) - - cmd = cmd["Launching"][0] # type: ignore - - # check that it's a domain by default - assert "NODE_TYPE=domain" in cmd or "NODE_TYPE='domain'" in cmd - - # check that the node has a name - assert "NODE_NAME=" in cmd - - # check that tail is on by default - assert " -d " not in cmd - - -def test_shortand_parse() -> None: - """This test is important because we want to make it convenient - for our users to just run 'hagrid launch' whenever they want to spin - up a new node with a randomly chosen name.""" - - # COMMAND: "hagrid launch" - args: tuple = () - args = grammar.launch_shorthand_support(args) - - # check that domain gets added to the end of the command - assert args == ("domain",) - - -def test_hagrid_launch_without_name_with_preposition() -> None: - """This test is important because we want to make it convenient - for our users to just run 'hagrid launch' whenever they want to spin - up a new node with a randomly chosen name""" - - # COMMAND: "hagrid launch on docker" - args: list[str] = ["to", "docker"] - - verb = cli.get_launch_verb() - grammar = cli.parse_grammar(args=tuple(args), verb=verb) - verb.load_grammar(grammar=grammar) - cmd = cli.create_launch_cmd( - verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True - ) - - cmd = cmd["Launching"][0] # type: ignore - # check that it's a domain by default - assert "NODE_TYPE=domain" in cmd or "NODE_TYPE='domain'" in cmd - - # check that the node has a name - assert "NODE_NAME=" in cmd - - # check that tail is on by default - assert " -d " not in cmd - - -def test_shortand_parse_without_name_with_preposition() -> None: - """This test is important because we want to make it convenient - for our users to just run 'hagrid launch' whenever they want to spin - up a new node with a randomly chosen name.""" - - # COMMAND: "hagrid launch" - args: tuple[str, ...] = ("to", "docker") - args = grammar.launch_shorthand_support(args) - - # check that domain gets added to the end of the command - assert args == ("domain", "to", "docker") - - -def test_launch_with_multiword_domain_name() -> None: - """This test is important because we want to make it convenient - for our users to just run 'hagrid launch' whenever they want to spin - up a new node with a randomly chosen name""" - - # COMMAND: "hagrid launch United Nations" - args: list[str] = ["United", "Nations"] - - verb = cli.get_launch_verb() - grammar = cli.parse_grammar(args=tuple(args), verb=verb) - verb.load_grammar(grammar=grammar) - cmd = cli.create_launch_cmd( - verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True - ) - - cmd = cmd["Launching"][0] # type: ignore - - # check that it's a domain by default - assert "NODE_TYPE=domain" in cmd or "NODE_TYPE='domain'" in cmd - - # check that the node has a name - assert "NODE_NAME=united_nations" in cmd or "NODE_NAME='united_nations'" in cmd - - # check that tail is on by default - assert " -d " not in cmd - - -def test_launch_with_longer_multiword_domain_name() -> None: - """This test is important because we want to make it convenient for users to launch nodes with - an arbitrary number of words.""" - - # COMMAND: "hagrid launch United Nations" - args: list[str] = ["United", "States", "of", "America"] - - verb = cli.get_launch_verb() - grammar = cli.parse_grammar(args=tuple(args), verb=verb) - verb.load_grammar(grammar=grammar) - cmd = cli.create_launch_cmd( - verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True - ) - - cmd = cmd["Launching"][0] # type: ignore - - # check that it's a domain by default - assert "NODE_TYPE=domain" in cmd or "NODE_TYPE='domain'" in cmd - - # check that the node has a name - assert ( - "NODE_NAME=united_states_of_america" in cmd - or "NODE_NAME='united_states_of_america'" in cmd - ) - - # check that tail is on by default - assert " -d " not in cmd - - -def test_launch_with_longer_multiword_domain_name_with_preposition() -> None: - """This test is important because we want to make it convenient for users to launch nodes with - an arbitrary number of words.""" - - # COMMAND: "hagrid launch United Nations on docker" - args: list[str] = ["United", "Nations", "to", "docker"] - - verb = cli.get_launch_verb() - grammar = cli.parse_grammar(args=tuple(args), verb=verb) - verb.load_grammar(grammar=grammar) - cmd = cli.create_launch_cmd( - verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True - ) - - cmd = cmd["Launching"][0] # type: ignore - - # check that it's a domain by default - assert "NODE_TYPE=domain" in cmd or "NODE_TYPE='domain'" in cmd - - # check that the node has a name - assert "NODE_NAME=united_nations" in cmd or "NODE_NAME='united_nations'" in cmd - - # check that tail is on by default - assert " -d " not in cmd - - -def test_shortand_parse_of_multiword_name() -> None: - """This test is important because we want to make it convenient - for our users to just run 'hagrid launch Multiple Word Name Of Node' whenever they want to spin - up a new node with a name that has multiple words.""" - - # COMMAND: "hagrid launch" - args: tuple[str, ...] = ("United", "Nations") - args = grammar.launch_shorthand_support(args) - - # check that domain gets added to the end of the command - assert args == ( - "United Nations", - "domain", - ) - - -def test_shortand_parse_of_multiword_name_with_domain() -> None: - """This test is important because we want to make it convenient - for our users to just run 'hagrid launch Multiple Word Name Of Node' whenever they want to spin - up a new node with a name that has multiple words.""" - - # COMMAND: "hagrid launch" - args: tuple[str, ...] = ("United", "Nations", "domain") - args = grammar.launch_shorthand_support(args) - - # check that domain gets added to the end of the command - assert args == ( - "United Nations", - "domain", - ) diff --git a/packages/log.txt b/packages/log.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/syft/.dockerignore b/packages/syft/.dockerignore deleted file mode 100644 index fcac49cb125..00000000000 --- a/packages/syft/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -.mypy_cache -**/.mypy_cache diff --git a/packages/syft/PYPI.md b/packages/syft/PYPI.md index 24f9dd81843..516d053e0e8 100644 --- a/packages/syft/PYPI.md +++ b/packages/syft/PYPI.md @@ -105,18 +105,11 @@ For Google GKE we need the [`gce` annotation](https://cloud.google.com/kubernete helm install ... --set ingress.class="gce" ``` -## Deploy to a Container Engine or Cloud +## Note: -1. Install our handy πŸ›΅ cli tool which makes deploying a Domain or Gateway server to Docker or VM a one-liner: - `pip install -U hagrid` +🚨 Our deployment tool `Hagrid` has been `Deprecated`. For the updated deployment options kindly refer to -2. Then run our interactive jupyter Install πŸ§™πŸ½β€β™‚οΈ WizardBETA: - `hagrid quickstart` - -3. In the tutorial you will learn how to install and deploy: - `PySyft` = our `numpy`-like 🐍 Python library for computing on `private data` in someone else's `Domain` - - `PyGrid` = our 🐳 `docker` / 🐧 `vm` `Domain` & `Gateway` Servers where `private data` lives +- πŸ“š Deployments ## Docs and Support @@ -125,10 +118,8 @@ helm install ... --set ingress.class="gce" # Install Notes -- HAGrid 0.3 Requires: 🐍 `python` πŸ™ `git` - Run: `pip install -U hagrid` -- Interactive Install πŸ§™πŸ½β€β™‚οΈ WizardBETA Requires πŸ›΅ `hagrid`: - Run: `hagrid quickstart` - PySyft 0.8.1 Requires: 🐍 `python 3.10 - 3.12` - Run: `pip install -U syft` -- PyGrid Requires: 🐳 `docker`, 🦦 `podman` or ☸️ `kubernetes` - Run: `hagrid launch ...` +- PyGrid Requires: 🐳 `docker`, 🦦 `podman` or ☸️ `kubernetes` # Versions @@ -151,13 +142,9 @@ Deprecated: PySyft and PyGrid use the same `version` and its best to match them up where possible. We release weekly betas which can be used in each context: -PySyft (Stable): `pip install -U syft` -PyGrid (Stable) `hagrid launch ... tag=latest` - -PySyft (Beta): `pip install -U syft --pre` -PyGrid (Beta): `hagrid launch ... tag=beta` +PySyft (Stable): `pip install -U syft` -HAGrid is a cli / deployment tool so the latest version of `hagrid` is usually the best. +PySyft (Beta): `pip install -U syft --pre` # What is Syft? diff --git a/packages/syft/setup.cfg b/packages/syft/setup.cfg index 9e72688e1c9..81b0272d8a3 100644 --- a/packages/syft/setup.cfg +++ b/packages/syft/setup.cfg @@ -52,7 +52,6 @@ syft = uvicorn[standard]==0.27.1 fastapi==0.110.0 psutil==5.9.8 - hagrid>=0.3 itables==1.7.1 argon2-cffi==23.1.0 matplotlib==3.8.3 @@ -68,7 +67,9 @@ syft = PyYAML==6.0.1 azure-storage-blob==12.19.1 ipywidgets==8.1.2 + rich==13.7.1 jinja2==3.1.4 + tenacity==8.3.0 install_requires = %(syft)s @@ -90,7 +91,8 @@ data_science = evaluate==0.4.1 recordlinkage==0.16 dm-haiku==0.0.10 - torch[cpu]==2.2.1 + # backend.dockerfile installs torch separately, so update the version over there as well! + torch==2.3.0 dev = %(test_plugins)s diff --git a/packages/syft/src/syft/__init__.py b/packages/syft/src/syft/__init__.py index 6ebbad50708..3aae76848dc 100644 --- a/packages/syft/src/syft/__init__.py +++ b/packages/syft/src/syft/__init__.py @@ -5,6 +5,7 @@ import pathlib from pathlib import Path import sys +from types import MethodType from typing import Any # relative @@ -15,7 +16,6 @@ from .client.client import login # noqa: F401 from .client.client import login_as_guest # noqa: F401 from .client.client import register # noqa: F401 -from .client.deploy import Orchestra # noqa: F401 from .client.domain_client import DomainClient # noqa: F401 from .client.gateway_client import GatewayClient # noqa: F401 from .client.registry import DomainRegistry # noqa: F401 @@ -33,6 +33,7 @@ from .node.server import serve_node # noqa: F401 from .node.server import serve_node as bind_worker # noqa: F401 from .node.worker import Worker # noqa: F401 +from .orchestra import Orchestra as orchestra # noqa: F401 from .protocol.data_protocol import bump_protocol_version # noqa: F401 from .protocol.data_protocol import check_or_stage_protocol # noqa: F401 from .protocol.data_protocol import get_data_protocol # noqa: F401 @@ -70,6 +71,7 @@ from .service.user.roles import Roles as roles # noqa: F401 from .service.user.user_service import UserService # noqa: F401 from .stable_version import LATEST_STABLE_SYFT +from .types.syft_object import SyftObject from .types.twin_object import TwinObject # noqa: F401 from .types.uid import UID # noqa: F401 from .util import filterwarnings # noqa: F401 @@ -109,6 +111,87 @@ pass # nosec +def _patch_ipython_autocompletion() -> None: + try: + # third party + from IPython.core.guarded_eval import EVALUATION_POLICIES + except ImportError: + return + + ipython = get_ipython() + if ipython is None: + return + + try: + # this allows property getters to be used in nested autocomplete + ipython.Completer.evaluation = "limited" + ipython.Completer.use_jedi = False + policy = EVALUATION_POLICIES["limited"] + + policy.allowed_getattr_external.update( + [ + ("syft.client.api", "APIModule"), + ("syft.client.api", "SyftAPI"), + ] + ) + original_can_get_attr = policy.can_get_attr + + def patched_can_get_attr(value: Any, attr: str) -> bool: + attr_name = "__syft_allow_autocomplete__" + # first check if exist to prevent side effects + if hasattr(value, attr_name) and attr in getattr(value, attr_name, []): + if attr in dir(value): + return True + else: + return False + else: + return original_can_get_attr(value, attr) + + policy.can_get_attr = patched_can_get_attr + except Exception: + print("Failed to patch ipython autocompletion for syft property getters") + + try: + # this constraints the completions for autocomplete. + # if __syft_dir__ is defined we only autocomplete those properties + # stdlib + import re + + original_attr_matches = ipython.Completer.attr_matches + + def patched_attr_matches(self, text: str) -> list[str]: # type: ignore + res = original_attr_matches(text) + m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer) + if not m2: + return res + expr, _ = m2.group(1, 2) + obj = self._evaluate_expr(expr) + if isinstance(obj, SyftObject) and hasattr(obj, "__syft_dir__"): + # here we filter all autocomplete results to only contain those + # defined in __syft_dir__, however the original autocomplete prefixes + # have the full path, while __syft_dir__ only defines the attr + attrs = set(obj.__syft_dir__()) + new_res = [] + for r in res: + splitted = r.split(".") + if len(splitted) > 1: + attr_name = splitted[-1] + if attr_name in attrs: + new_res.append(r) + return new_res + else: + return res + + ipython.Completer.attr_matches = MethodType( + patched_attr_matches, ipython.Completer + ) + except Exception: + print("Failed to patch syft autocompletion for __syft_dir__") + + +_patch_ipython_autocompletion() + + def module_property(func: Any) -> Callable: """Decorator to turn module functions into properties. Function names must be prefixed with an underscore.""" @@ -149,11 +232,6 @@ def _settings() -> UserSettings: return settings -@module_property -def _orchestra() -> Orchestra: - return Orchestra - - @module_property def hello_baby() -> None: print("Hello baby!") diff --git a/packages/syft/src/syft/client/api.py b/packages/syft/src/syft/client/api.py index a92957cd959..00b0673f221 100644 --- a/packages/syft/src/syft/client/api.py +++ b/packages/syft/src/syft/client/api.py @@ -48,6 +48,7 @@ from ..service.warnings import WarningContext from ..types.cache_object import CachedSyftObject from ..types.identity import Identity +from ..types.syft_object import SYFT_OBJECT_VERSION_1 from ..types.syft_object import SYFT_OBJECT_VERSION_2 from ..types.syft_object import SyftBaseObject from ..types.syft_object import SyftMigrationRegistry @@ -56,6 +57,7 @@ from ..types.uid import UID from ..util.autoreload import autoreload_enabled from ..util.markdown import as_markdown_python_code +from ..util.table import list_dict_repr_html from ..util.telemetry import instrument from ..util.util import prompt_warning_message from .connection import NodeConnection @@ -66,6 +68,21 @@ from ..service.job.job_stash import Job +IPYNB_BACKGROUND_METHODS = { + "getdoc", + "_partialmethod", + "__name__", + "__code__", + "__wrapped__", + "__custom_documentations__", + "__signature__", + "__defaults__", + "__kwdefaults__", +} + +IPYNB_BACKGROUND_PREFIXES = ["_ipy", "_repr", "__ipython", "__pydantic"] + + class APIRegistry: __api_registry__: dict[tuple, SyftAPI] = OrderedDict() @@ -585,6 +602,19 @@ def wrapper(*args: Any, **kwargs: Any) -> SyftError | Any: return wrapper +class APISubModulesView(SyftObject): + __canonical_name__ = "APISubModulesView" + __version__ = SYFT_OBJECT_VERSION_1 + + submodule: str = "" + endpoints: list[str] = [] + + __syft_include_id_coll_repr__ = False + + def _coll_repr_(self) -> dict[str, Any]: + return {"submodule": self.submodule, "endpoints": "\n".join(self.endpoints)} + + @serializable() class APIModule: _modules: list[str] @@ -596,6 +626,9 @@ def __init__(self, path: str, refresh_callback: Callable | None) -> None: self.path = path self.refresh_callback = refresh_callback + def __dir__(self) -> list[str]: + return self._modules + ["path"] + def has_submodule(self, name: str) -> bool: """We use this as hasattr() triggers __getattribute__ which triggers recursion""" try: @@ -610,18 +643,28 @@ def _add_submodule( setattr(self, attr_name, module_or_func) self._modules.append(attr_name) - def __getattribute__(self, name: str) -> Any: + def __getattr__(self, name: str) -> Any: try: return object.__getattribute__(self, name) except AttributeError: # if we fail, we refresh the api and try again - if self.refresh_callback is not None: + # however, we dont want this to happen all the time because of ipy magic happening + # in the background + if ( + self.refresh_callback is not None + and name not in IPYNB_BACKGROUND_METHODS + and not any( + name.startswith(prefix) for prefix in IPYNB_BACKGROUND_PREFIXES + ) + ): api = self.refresh_callback() try: + # get current path in the module tree new_current_module = api.services for submodule in self.path.split("."): if submodule != "": new_current_module = getattr(new_current_module, submodule) + # retry getting the attribute, if this fails, we throw an error return object.__getattribute__(new_current_module, name) except AttributeError: pass @@ -641,7 +684,31 @@ def _repr_html_(self) -> Any: return self.get()._repr_html_() if not hasattr(self, "get_all"): - return NotImplementedError + + def recursively_get_submodules( + module: APIModule | Callable, + ) -> list[APIModule | Callable]: + children = [module] + if isinstance(module, APIModule): + for submodule_name in module._modules: + submodule = getattr(module, submodule_name) + children += recursively_get_submodules(submodule) + return children + + views = [] + for submodule_name in self._modules: + submodule = getattr(self, submodule_name) + children = recursively_get_submodules(submodule) + child_paths = [ + x.path for x in children if isinstance(x, RemoteFunction) + ] + views.append( + APISubModulesView(submodule=submodule_name, endpoints=child_paths) + ) + + return list_dict_repr_html(views) + # return NotImplementedError + results = self.get_all() return results._repr_html_() @@ -767,8 +834,26 @@ class SyftAPI(SyftObject): __user_role: ServiceRole = ServiceRole.NONE communication_protocol: PROTOCOL_TYPE - # def __post_init__(self) -> None: - # pass + # informs getattr does not have nasty side effects + __syft_allow_autocomplete__ = ["services"] + + def __dir__(self) -> list[str]: + modules = getattr(self.api_module, "_modules", []) + return ["services"] + modules + + def __syft_dir__(self) -> list[str]: + modules = getattr(self.api_module, "_modules", []) + return ["services"] + modules + + def __getattr__(self, name: str) -> Any: + try: + return getattr(self.api_module, name) + except Exception: + raise SyftAttributeError( + f"'SyftAPI' object has no submodule or method '{name}', " + "you may not have permission to access the module you are trying to access." + "If you think this is an error, try calling `client.refresh()` to update the API." + ) @staticmethod def for_user( diff --git a/packages/syft/src/syft/client/client.py b/packages/syft/src/syft/client/client.py index 60289c39232..ba4dfc38c80 100644 --- a/packages/syft/src/syft/client/client.py +++ b/packages/syft/src/syft/client/client.py @@ -8,7 +8,6 @@ from enum import Enum from getpass import getpass import json -import os from typing import Any from typing import TYPE_CHECKING from typing import cast @@ -485,6 +484,17 @@ class SyftClient: __logged_in_username: str = "" __user_role: ServiceRole = ServiceRole.NONE + # informs getattr does not have nasty side effects + __syft_allow_autocomplete__ = [ + "api", + "code", + "jobs", + "users", + "settings", + "notifications", + "custom_api", + ] + def __init__( self, connection: NodeConnection, @@ -496,6 +506,7 @@ def __init__( self.metadata = metadata self.credentials: SyftSigningKey | None = credentials self._api = api + self.services: APIModule | None = None self.communication_protocol: int | str | None = None self.current_protocol: int | str | None = None @@ -547,7 +558,7 @@ def create_project( user_email_address=user_email_address, members=[self], ) - project = project_create.start() + project = project_create.send() return project # TODO: type of request should be REQUEST, but it will give circular import error @@ -766,15 +777,6 @@ def login( register: bool = False, **kwargs: Any, ) -> Self: - # TODO: Remove this Hack (Note to Rasswanth) - # If SYFT_LOGIN_{NODE_NAME}_PASSWORD is set, use that as the password - # for the login. This is useful for CI/CD environments to test password - # randomization that is implemented by helm charts - if self.name is not None and email == "info@openmined.org": - pass_env_var = f"SYFT_LOGIN_{self.name}_PASSWORD" - if pass_env_var in os.environ: - password = os.environ[pass_env_var] - if email is None: email = input("Email: ") if password is None: @@ -956,6 +958,7 @@ def refresh_callback() -> SyftAPI: api=_api, ) self._api = _api + self.services = _api.services return _api diff --git a/packages/syft/src/syft/client/deploy.py b/packages/syft/src/syft/client/deploy.py deleted file mode 100644 index bd19895ced5..00000000000 --- a/packages/syft/src/syft/client/deploy.py +++ /dev/null @@ -1,33 +0,0 @@ -# stdlib -from typing import Any - -# relative -from ..service.response import SyftError - - -class InstallOrchestra: - def launch(self, *args: Any, **kwargs: Any) -> None: - return self.error() - - def error(self) -> Any: - message = "Please install hagrid with `pip install -U hagrid`" - return SyftError(message=message) - - def _repr_html_(self) -> str: - return self.error()._repr_html_() - - -def import_orchestra() -> Any: - try: - # third party - from hagrid import Orchestra - - return Orchestra - - except Exception as e: # nosec - print(e) - pass - return InstallOrchestra() - - -Orchestra = import_orchestra() diff --git a/packages/syft/src/syft/client/domain_client.py b/packages/syft/src/syft/client/domain_client.py index 4843cf9d41d..75ec142bfde 100644 --- a/packages/syft/src/syft/client/domain_client.py +++ b/packages/syft/src/syft/client/domain_client.py @@ -8,7 +8,6 @@ from typing import cast # third party -from hagrid.orchestra import NodeHandle from loguru import logger from tqdm import tqdm @@ -43,6 +42,7 @@ if TYPE_CHECKING: # relative + from ..orchestra import NodeHandle from ..service.project.project import Project diff --git a/packages/syft/src/syft/client/enclave_client.py b/packages/syft/src/syft/client/enclave_client.py index ddf61c30f86..32eebdf3189 100644 --- a/packages/syft/src/syft/client/enclave_client.py +++ b/packages/syft/src/syft/client/enclave_client.py @@ -5,9 +5,6 @@ from typing import Any from typing import TYPE_CHECKING -# third party -from hagrid.orchestra import NodeHandle - # relative from ..abstract_node import NodeSideType from ..client.api import APIRegistry @@ -29,6 +26,7 @@ if TYPE_CHECKING: # relative + from ..orchestra import NodeHandle from ..service.code.user_code import SubmitUserCode diff --git a/packages/syft/src/syft/client/syncing.py b/packages/syft/src/syft/client/syncing.py index a48cef05ab3..371b77df22a 100644 --- a/packages/syft/src/syft/client/syncing.py +++ b/packages/syft/src/syft/client/syncing.py @@ -1,5 +1,4 @@ # stdlib -import warnings # relative from ..abstract_node import NodeSideType @@ -12,6 +11,7 @@ from ..service.sync.resolve_widget import ResolveWidget from ..service.sync.sync_state import SyncState from ..types.uid import UID +from ..util.decorators import deprecated from .client import SyftClient from .sync_decision import SyncDecision from .sync_decision import SyncDirection @@ -69,12 +69,8 @@ def resolve(obj_diff_batch: ObjectDiffBatch) -> ResolveWidget: return widget +@deprecated(reason="resolve_single has been renamed to resolve", return_syfterror=True) def resolve_single(obj_diff_batch: ObjectDiffBatch) -> ResolveWidget: - warnings.warn( - "resolve_single has been renamed to resolve", - DeprecationWarning, - stacklevel=1, - ) return resolve(obj_diff_batch) diff --git a/packages/syft/src/syft/node/node.py b/packages/syft/src/syft/node/node.py index 7861ee422e0..64d10f92930 100644 --- a/packages/syft/src/syft/node/node.py +++ b/packages/syft/src/syft/node/node.py @@ -62,6 +62,7 @@ from ..service.job.job_service import JobService from ..service.job.job_stash import Job from ..service.job.job_stash import JobStash +from ..service.job.job_stash import JobType from ..service.log.log_service import LogService from ..service.metadata.metadata_service import MetadataService from ..service.metadata.node_metadata import NodeMetadataV3 @@ -372,10 +373,12 @@ def __init__( use_sqlite = local_db or (processes > 0 and not is_subprocess) document_store_config = document_store_config or self.get_default_store( - use_sqlite=use_sqlite + use_sqlite=use_sqlite, + store_type="Document Store", ) action_store_config = action_store_config or self.get_default_store( - use_sqlite=use_sqlite + use_sqlite=use_sqlite, + store_type="Action Store", ) self.init_stores( action_store_config=action_store_config, @@ -434,12 +437,15 @@ def runs_in_docker(self) -> bool: and any("docker" in line for line in open(path)) ) - def get_default_store(self, use_sqlite: bool) -> StoreConfig: + def get_default_store(self, use_sqlite: bool, store_type: str) -> StoreConfig: if use_sqlite: path = self.get_temp_dir("db") + file_name: str = f"{self.id}.sqlite" + if self.dev_mode: + print(f"{store_type}'s SQLite DB path: {path/file_name}") return SQLiteStoreConfig( client_config=SQLiteStoreClientConfig( - filename=f"{self.id}.sqlite", + filename=file_name, path=path, ) ) @@ -1283,10 +1289,10 @@ def add_api_endpoint_execution_to_queue( action = Action.from_api_endpoint_execution() return self.add_queueitem_to_queue( - queue_item, - credentials, - action, - None, + queue_item=queue_item, + credentials=credentials, + action=action, + job_type=JobType.TWINAPIJOB, ) def get_worker_pool_ref_by_name( @@ -1355,16 +1361,22 @@ def add_action_to_queue( ) return self.add_queueitem_to_queue( - queue_item, credentials, action, parent_job_id, user_id + queue_item=queue_item, + credentials=credentials, + action=action, + parent_job_id=parent_job_id, + user_id=user_id, ) def add_queueitem_to_queue( self, + *, queue_item: QueueItem, credentials: SyftVerifyKey, action: Action | None = None, parent_job_id: UID | None = None, user_id: UID | None = None, + job_type: JobType = JobType.JOB, ) -> Job | SyftError: log_id = UID() role = self.get_role_for_credentials(credentials=credentials) @@ -1398,6 +1410,7 @@ def add_queueitem_to_queue( parent_job_id=parent_job_id, action=action, requested_by=user_id, + job_type=job_type, ) # 🟑 TODO 36: Needs distributed lock @@ -1500,8 +1513,8 @@ def add_api_call_to_queue( worker_pool=worker_pool_ref, ) return self.add_queueitem_to_queue( - queue_item, - api_call.credentials, + queue_item=queue_item, + credentials=api_call.credentials, action=None, parent_job_id=parent_job_id, ) diff --git a/packages/syft/src/syft/node/run.py b/packages/syft/src/syft/node/run.py index d82d88c9a97..5d731d48fd5 100644 --- a/packages/syft/src/syft/node/run.py +++ b/packages/syft/src/syft/node/run.py @@ -1,11 +1,9 @@ # stdlib import argparse -# third party -from hagrid.orchestra import NodeHandle - # relative -from ..client.deploy import Orchestra +from ..orchestra import NodeHandle +from ..orchestra import Orchestra def str_to_bool(bool_str: str | None) -> bool: @@ -71,16 +69,8 @@ def run() -> NodeHandle | None: default="True", dest="tail", ) - parser.add_argument( - "--cmd", - help="cmd mode", - type=str, - default="False", - dest="cmd", - ) args = parser.parse_args() - if args.command != "launch": print("syft launch is the only command currently supported") @@ -100,7 +90,6 @@ def run() -> NodeHandle | None: local_db=args.local_db, processes=args.processes, tail=args.tail, - cmd=args.cmd, ) if not args.tail: return node diff --git a/packages/syft/src/syft/orchestra.py b/packages/syft/src/syft/orchestra.py new file mode 100644 index 00000000000..1a08f594aa2 --- /dev/null +++ b/packages/syft/src/syft/orchestra.py @@ -0,0 +1,331 @@ +"""Python Level API to launch Syft services.""" + +# future +from __future__ import annotations + +# stdlib +from collections.abc import Callable +from enum import Enum +import getpass +import inspect +import os +import sys +from typing import Any + +# relative +from .abstract_node import NodeSideType +from .abstract_node import NodeType +from .client.client import login_as_guest as sy_login_as_guest +from .node.domain import Domain +from .node.enclave import Enclave +from .node.gateway import Gateway +from .node.server import serve_node +from .protocol.data_protocol import stage_protocol_changes +from .service.response import SyftError +from .util.util import get_random_available_port + +DEFAULT_PORT = 8080 +DEFAULT_URL = "http://localhost" + +ClientAlias = Any # we don't want to import Client in case it changes + + +def get_node_type(node_type: str | NodeType | None) -> NodeType | None: + if node_type is None: + node_type = os.environ.get("ORCHESTRA_NODE_TYPE", NodeType.DOMAIN) + try: + return NodeType(node_type) + except ValueError: + print(f"node_type: {node_type} is not a valid NodeType: {NodeType}") + return None + + +def get_deployment_type(deployment_type: str | None) -> DeploymentType | None: + if deployment_type is None: + deployment_type = os.environ.get( + "ORCHESTRA_DEPLOYMENT_TYPE", DeploymentType.PYTHON + ) + + try: + return DeploymentType(deployment_type) + except ValueError: + print( + f"deployment_type: {deployment_type} is not a valid DeploymentType: {DeploymentType}" + ) + return None + + +# Can also be specified by the environment variable +# ORCHESTRA_DEPLOYMENT_TYPE +class DeploymentType(Enum): + PYTHON = "python" + REMOTE = "remote" + + +class NodeHandle: + def __init__( + self, + node_type: NodeType, + deployment_type: DeploymentType, + node_side_type: NodeSideType, + name: str, + port: int | None = None, + url: str | None = None, + python_node: Any | None = None, + shutdown: Callable | None = None, + ) -> None: + self.node_type = node_type + self.name = name + self.port = port + self.url = url + self.python_node = python_node + self.shutdown = shutdown + self.deployment_type = deployment_type + self.node_side_type = node_side_type + + @property + def client(self) -> Any: + if self.port: + return sy_login_as_guest(url=self.url, port=self.port) # type: ignore + elif self.deployment_type == DeploymentType.PYTHON: + return self.python_node.get_guest_client(verbose=False) # type: ignore + else: + raise NotImplementedError( + f"client not implemented for the deployment type:{self.deployment_type}" + ) + + def login_as_guest(self, **kwargs: Any) -> ClientAlias: + return self.client.login_as_guest(**kwargs) + + def login( + self, email: str | None = None, password: str | None = None, **kwargs: Any + ) -> ClientAlias: + if not email: + email = input("Email: ") + + if not password: + password = getpass.getpass("Password: ") + + return self.client.login(email=email, password=password, **kwargs) + + def register( + self, + name: str, + email: str | None = None, + password: str | None = None, + password_verify: str | None = None, + institution: str | None = None, + website: str | None = None, + ) -> Any: + if not email: + email = input("Email: ") + if not password: + password = getpass.getpass("Password: ") + if not password_verify: + password_verify = getpass.getpass("Confirm Password: ") + if password != password_verify: + return SyftError(message="Passwords do not match") + + client = self.client + return client.register( + name=name, + email=email, + password=password, + institution=institution, + password_verify=password_verify, + website=website, + ) + + def land(self) -> None: + if self.deployment_type == DeploymentType.PYTHON: + if self.shutdown: + self.shutdown() + else: + print( + f"Shutdown not implemented for the deployment type:{self.deployment_type}", + file=sys.stderr, + ) + + +def deploy_to_python( + node_type_enum: NodeType, + deployment_type_enum: DeploymentType, + port: int | str, + name: str, + host: str, + reset: bool, + tail: bool, + dev_mode: bool, + processes: int, + local_db: bool, + node_side_type: NodeSideType, + enable_warnings: bool, + n_consumers: int, + thread_workers: bool, + create_producer: bool = False, + queue_port: int | None = None, + association_request_auto_approval: bool = False, + background_tasks: bool = False, +) -> NodeHandle: + worker_classes = { + NodeType.DOMAIN: Domain, + NodeType.GATEWAY: Gateway, + NodeType.ENCLAVE: Enclave, + } + + if dev_mode: + print("Staging Protocol Changes...") + stage_protocol_changes() + + kwargs = { + "name": name, + "host": host, + "port": port, + "reset": reset, + "processes": processes, + "dev_mode": dev_mode, + "tail": tail, + "node_type": node_type_enum, + "node_side_type": node_side_type, + "enable_warnings": enable_warnings, + "queue_port": queue_port, + "n_consumers": n_consumers, + "create_producer": create_producer, + "association_request_auto_approval": association_request_auto_approval, + "background_tasks": background_tasks, + } + + if port: + kwargs["in_memory_workers"] = True + if port == "auto": + port = get_random_available_port() + kwargs["port"] = port + + sig = inspect.signature(serve_node) + supported_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} + + start, stop = serve_node(**supported_kwargs) + start() + return NodeHandle( + node_type=node_type_enum, + deployment_type=deployment_type_enum, + name=name, + port=port, + url="http://localhost", + shutdown=stop, + node_side_type=node_side_type, + ) + else: + kwargs["local_db"] = local_db + kwargs["thread_workers"] = thread_workers + if node_type_enum in worker_classes: + worker_class = worker_classes[node_type_enum] + sig = inspect.signature(worker_class.named) + supported_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} + if "node_type" in sig.parameters.keys() and "migrate" in sig.parameters: + supported_kwargs["migrate"] = True + worker = worker_class.named(**supported_kwargs) + else: + raise NotImplementedError(f"node_type: {node_type_enum} is not supported") + + def stop() -> None: + worker.stop() + + return NodeHandle( + node_type=node_type_enum, + deployment_type=deployment_type_enum, + name=name, + python_node=worker, + node_side_type=node_side_type, + shutdown=stop, + ) + + +def deploy_to_remote( + node_type_enum: NodeType, + deployment_type_enum: DeploymentType, + name: str, + node_side_type: NodeSideType, +) -> NodeHandle: + node_port = int(os.environ.get("NODE_PORT", f"{DEFAULT_PORT}")) + node_url = str(os.environ.get("NODE_URL", f"{DEFAULT_URL}")) + return NodeHandle( + node_type=node_type_enum, + deployment_type=deployment_type_enum, + name=name, + port=node_port, + url=node_url, + node_side_type=node_side_type, + ) + + +class Orchestra: + @staticmethod + def launch( + # node information and deployment + name: str | None = None, + node_type: str | NodeType | None = None, + deploy_to: str | None = None, + node_side_type: str | None = None, + # worker related inputs + port: int | str | None = None, + processes: int = 1, # temporary work around for jax in subprocess + local_db: bool = False, + dev_mode: bool = False, + reset: bool = False, + tail: bool = False, + host: str | None = "0.0.0.0", # nosec + enable_warnings: bool = False, + n_consumers: int = 0, + thread_workers: bool = False, + create_producer: bool = False, + queue_port: int | None = None, + association_request_auto_approval: bool = False, + background_tasks: bool = False, + ) -> NodeHandle: + if dev_mode is True: + thread_workers = True + os.environ["DEV_MODE"] = str(dev_mode) + + node_type_enum: NodeType | None = get_node_type(node_type=node_type) + node_side_type_enum = ( + NodeSideType.HIGH_SIDE + if node_side_type is None + else NodeSideType(node_side_type) + ) + + deployment_type_enum: DeploymentType | None = get_deployment_type( + deployment_type=deploy_to + ) + + if deployment_type_enum == DeploymentType.PYTHON: + return deploy_to_python( + node_type_enum=node_type_enum, + deployment_type_enum=deployment_type_enum, + port=port, + name=name, + host=host, + reset=reset, + tail=tail, + dev_mode=dev_mode, + processes=processes, + local_db=local_db, + node_side_type=node_side_type_enum, + enable_warnings=enable_warnings, + n_consumers=n_consumers, + thread_workers=thread_workers, + create_producer=create_producer, + queue_port=queue_port, + association_request_auto_approval=association_request_auto_approval, + background_tasks=background_tasks, + ) + elif deployment_type_enum == DeploymentType.REMOTE: + return deploy_to_remote( + node_type_enum=node_type_enum, + deployment_type_enum=deployment_type_enum, + name=name, + node_side_type=node_side_type_enum, + ) + raise NotImplementedError( + f"deployment_type: {deployment_type_enum} is not supported" + ) diff --git a/packages/syft/src/syft/protocol/protocol_version.json b/packages/syft/src/syft/protocol/protocol_version.json index e30f48dfd5a..5df701ca714 100644 --- a/packages/syft/src/syft/protocol/protocol_version.json +++ b/packages/syft/src/syft/protocol/protocol_version.json @@ -52,9 +52,9 @@ "hash": "6a7cc7c2bb4dd234c1508b0af4d3b403cd3b7b427578a775bf80dc36891923ed", "action": "remove" }, - "5": { - "version": 5, - "hash": "82ee08442b09797ed7a3710c31de633bb308b1d2215f51b58a3e01a4c201055d", + "6": { + "version": 6, + "hash": "865a2ed791b8abd20d76e9a6bfae7ae7dad51b5ebfd8ff728aab25af93fa5570", "action": "add" } }, diff --git a/packages/syft/src/syft/service/code/user_code.py b/packages/syft/src/syft/service/code/user_code.py index b840f94f83a..c779037a6c9 100644 --- a/packages/syft/src/syft/service/code/user_code.py +++ b/packages/syft/src/syft/service/code/user_code.py @@ -82,6 +82,7 @@ from ..response import SyftNotReady from ..response import SyftSuccess from ..response import SyftWarning +from ..user.user import UserView from .code_parse import GlobalsVisitor from .code_parse import LaunchJobVisitor from .unparse import unparse @@ -348,6 +349,18 @@ def _coll_repr_(self) -> dict[str, Any]: "Submit time": str(self.submit_time), } + @property + def user(self) -> UserView | SyftError: + api = APIRegistry.api_for( + node_uid=self.syft_node_location, + user_verify_key=self.user_verify_key, + ) + if api is None: + return SyftError( + message=f"Can't access Syft API. You must login to {self.syft_node_location}" + ) + return api.services.user.get_current_user() + @property def status(self) -> UserCodeStatusCollection | SyftError: # Clientside only @@ -825,7 +838,7 @@ def _ephemeral_node_call( **kwargs: Any, ) -> Any: # relative - from ... import _orchestra + from ...orchestra import Orchestra # Right now we only create a number of workers # In the future we might need to have the same pools/images as well @@ -839,7 +852,7 @@ def _ephemeral_node_call( time_alive = 300 # This could be changed given the work on containers - ep_node = _orchestra().launch( + ep_node = Orchestra.launch( name=f"ephemeral_node_{self.func_name}_{random.randint(a=0, b=10000)}", # nosec reset=True, create_producer=True, diff --git a/packages/syft/src/syft/service/job/job_stash.py b/packages/syft/src/syft/service/job/job_stash.py index d7aa3aca00b..2943913cf73 100644 --- a/packages/syft/src/syft/service/job/job_stash.py +++ b/packages/syft/src/syft/service/job/job_stash.py @@ -1,6 +1,7 @@ # stdlib from datetime import datetime from datetime import timedelta +from datetime import timezone from enum import Enum import random from string import Template @@ -28,8 +29,9 @@ from ...store.document_store import QueryKeys from ...store.document_store import UIDPartitionKey from ...types.datetime import DateTime +from ...types.datetime import format_timedelta from ...types.syft_object import SYFT_OBJECT_VERSION_2 -from ...types.syft_object import SYFT_OBJECT_VERSION_5 +from ...types.syft_object import SYFT_OBJECT_VERSION_6 from ...types.syft_object import SyftObject from ...types.syncable_object import SyncableSyftObject from ...types.uid import UID @@ -73,10 +75,19 @@ def center_content(text: Any) -> str: return center_div +@serializable() +class JobType(str, Enum): + JOB = "job" + TWINAPIJOB = "twinapijob" + + def __str__(self) -> str: + return self.value + + @serializable() class Job(SyncableSyftObject): __canonical_name__ = "JobItem" - __version__ = SYFT_OBJECT_VERSION_5 + __version__ = SYFT_OBJECT_VERSION_6 id: UID node_uid: UID @@ -87,13 +98,16 @@ class Job(SyncableSyftObject): parent_job_id: UID | None = None n_iters: int | None = 0 current_iter: int | None = None - creation_time: str | None = Field(default_factory=lambda: str(datetime.now())) + creation_time: str | None = Field( + default_factory=lambda: str(datetime.now(tz=timezone.utc)) + ) action: Action | None = None job_pid: int | None = None job_worker_id: UID | None = None updated_at: DateTime | None = None user_code_id: UID | None = None requested_by: UID | None = None + job_type: JobType = JobType.JOB __attr_searchable__ = ["parent_job_id", "job_worker_id", "status", "user_code_id"] __repr_attrs__ = [ @@ -191,18 +205,7 @@ def eta_string(self) -> str | None: ): return None - def format_timedelta(local_timedelta: timedelta) -> str: - total_seconds = int(local_timedelta.total_seconds()) - hours, leftover = divmod(total_seconds, 3600) - minutes, seconds = divmod(leftover, 60) - - hours_string = f"{hours}:" if hours != 0 else "" - minutes_string = f"{minutes}:".zfill(3) - seconds_string = f"{seconds}".zfill(2) - - return f"{hours_string}{minutes_string}{seconds_string}" - - now = datetime.now() + now = datetime.now(tz=timezone.utc) time_passed = now - datetime.fromisoformat(self.creation_time) iter_duration_seconds: float = time_passed.total_seconds() / self.current_iter iters_remaining = self.n_iters - self.current_iter diff --git a/packages/syft/src/syft/service/project/project.py b/packages/syft/src/syft/service/project/project.py index aa8048f788e..d9b84ef9f15 100644 --- a/packages/syft/src/syft/service/project/project.py +++ b/packages/syft/src/syft/service/project/project.py @@ -38,6 +38,7 @@ from ...types.uid import UID from ...util import options from ...util.colors import SURFACE +from ...util.decorators import deprecated from ...util.markdown import markdown_as_class_with_fields from ...util.util import full_name_with_qualname from ..code.user_code import SubmitUserCode @@ -1261,7 +1262,13 @@ def create_code_request( reason=reason, ) + @deprecated( + reason="Project.start has been renamed to Project.send", return_syfterror=True + ) def start(self, return_all_projects: bool = False) -> Project | list[Project]: + return self.send(return_all_projects=return_all_projects) + + def send(self, return_all_projects: bool = False) -> Project | list[Project]: # Currently we are assuming that the first member is the leader # This would be changed in our future leaderless approach leader = self.clients[0] diff --git a/packages/syft/src/syft/service/request/request.py b/packages/syft/src/syft/service/request/request.py index 5d10f6cc75e..889491278e8 100644 --- a/packages/syft/src/syft/service/request/request.py +++ b/packages/syft/src/syft/service/request/request.py @@ -467,9 +467,9 @@ def _coll_repr_(self) -> dict[str, str | dict[str, str]]: ] return { - "Request time": str(self.request_time), "Description": self.html_description, "Requested By": "\n".join(user_data), + "Creation Time": str(self.request_time), "Status": status_badge, } @@ -1220,18 +1220,19 @@ def nested_repr(self, node: Any | None = None, level: int = 0) -> str: def __repr_syft_nested__(self) -> str: msg = ( - f"Request to change {self.code.service_func_name} " - f"(Pool Id: {self.code.worker_pool_name}) " + f"Request to change {self.code.service_func_name} " + f"(Pool Id: {self.code.worker_pool_name}) " ) - msg += "to permission RequestStatus.APPROVED." - if self.nested_solved: - if self.link.nested_codes == {}: # type: ignore - msg += "No nested requests." - else: + msg += "to permission RequestStatus.APPROVED." + if self.code.nested_codes is None or self.code.nested_codes == {}: # type: ignore + msg += " No nested requests" + else: + if self.nested_solved: + # else: msg += "

This change requests the following nested functions calls:
" msg += self.nested_repr() - else: - msg += "Nested Requests not resolved." + else: + msg += " Nested Requests not resolved" return msg def _repr_markdown_(self, wrap_as_python: bool = True, indent: int = 0) -> str: diff --git a/packages/syft/src/syft/service/sync/diff_state.py b/packages/syft/src/syft/service/sync/diff_state.py index 014e33f5bc8..cde79262c24 100644 --- a/packages/syft/src/syft/service/sync/diff_state.py +++ b/packages/syft/src/syft/service/sync/diff_state.py @@ -45,6 +45,7 @@ from ..code.user_code import UserCode from ..code.user_code import UserCodeStatusCollection from ..job.job_stash import Job +from ..job.job_stash import JobType from ..log.log import SyftLog from ..output.output_service import ExecutionOutput from ..request.request import Request @@ -1288,7 +1289,12 @@ def hierarchies( # TODO: Figure out nested user codes, do we even need that? root_ids.append(diff.object_id) # type: ignore - elif isinstance(diff_obj, Job) and diff_obj.parent_job_id is None: # type: ignore + elif ( + isinstance(diff_obj, Job) # type: ignore + and diff_obj.parent_job_id is None + # ignore Job objects created by TwinAPIEndpoint + and diff_obj.job_type != JobType.TWINAPIJOB + ): root_ids.append(diff.object_id) # type: ignore for root_uid in root_ids: diff --git a/packages/syft/src/syft/store/blob_storage/seaweedfs.py b/packages/syft/src/syft/store/blob_storage/seaweedfs.py index 74762c4155f..e31adc18b7d 100644 --- a/packages/syft/src/syft/store/blob_storage/seaweedfs.py +++ b/packages/syft/src/syft/store/blob_storage/seaweedfs.py @@ -11,7 +11,12 @@ from botocore.client import BaseClient as S3BaseClient from botocore.client import ClientError as BotoClientError from botocore.client import Config +from botocore.exceptions import ConnectionError import requests +from tenacity import retry +from tenacity import retry_if_exception_type +from tenacity import stop_after_delay +from tenacity import wait_fixed from tqdm import tqdm from typing_extensions import Self @@ -215,12 +220,22 @@ def __init__( self.default_bucket_name = default_bucket_name self.config = config + self._check_connection() + def __enter__(self) -> Self: return self def __exit__(self, *exc: Any) -> None: self.client.close() + @retry( + wait=wait_fixed(5), + stop=stop_after_delay(60), + retry=retry_if_exception_type(ConnectionError), + ) + def _check_connection(self) -> dict: + return self.client.list_buckets() + def read( self, fp: SecureFilePathLocation, diff --git a/packages/syft/src/syft/types/datetime.py b/packages/syft/src/syft/types/datetime.py index 7b66f40f24a..65feab6633f 100644 --- a/packages/syft/src/syft/types/datetime.py +++ b/packages/syft/src/syft/types/datetime.py @@ -1,5 +1,6 @@ # stdlib from datetime import datetime +from datetime import timedelta from functools import total_ordering import re from typing import Any @@ -57,3 +58,34 @@ def __eq__(self, other: Any) -> bool: def __lt__(self, other: Self) -> bool: return self.utc_timestamp < other.utc_timestamp + + def timedelta(self, other: "DateTime") -> timedelta: + utc_timestamp_delta = self.utc_timestamp - other.utc_timestamp + return timedelta(seconds=utc_timestamp_delta) + + +def format_timedelta(local_timedelta: timedelta) -> str: + total_seconds = int(local_timedelta.total_seconds()) + hours, leftover = divmod(total_seconds, 3600) + minutes, seconds = divmod(leftover, 60) + + hours_string = f"{hours}:" if hours != 0 else "" + minutes_string = f"{minutes}:".zfill(3) + seconds_string = f"{seconds}".zfill(2) + + return f"{hours_string}{minutes_string}{seconds_string}" + + +def format_timedelta_human_readable(local_timedelta: timedelta) -> str: + # Returns a human-readable string representing the timedelta + units = [("day", 86400), ("hour", 3600), ("minute", 60), ("second", 1)] + total_seconds = int(local_timedelta.total_seconds()) + + for unit_name, unit_seconds in units: + unit_value, total_seconds = divmod(total_seconds, unit_seconds) + if unit_value > 0: + if unit_value == 1: + return f"{unit_value} {unit_name}" + else: + return f"{unit_value} {unit_name}s" + return "0 seconds" diff --git a/packages/syft/src/syft/types/syft_object.py b/packages/syft/src/syft/types/syft_object.py index a290e4ff080..3ec9c073165 100644 --- a/packages/syft/src/syft/types/syft_object.py +++ b/packages/syft/src/syft/types/syft_object.py @@ -61,6 +61,7 @@ SYFT_OBJECT_VERSION_3 = 3 SYFT_OBJECT_VERSION_4 = 4 SYFT_OBJECT_VERSION_5 = 5 +SYFT_OBJECT_VERSION_6 = 6 supported_object_versions = [ SYFT_OBJECT_VERSION_1, @@ -68,6 +69,7 @@ SYFT_OBJECT_VERSION_3, SYFT_OBJECT_VERSION_4, SYFT_OBJECT_VERSION_5, + SYFT_OBJECT_VERSION_6, ] HIGHEST_SYFT_OBJECT_VERSION = max(supported_object_versions) diff --git a/packages/syft/src/syft/util/decorators.py b/packages/syft/src/syft/util/decorators.py index 1262099d1c6..acfeba490e8 100644 --- a/packages/syft/src/syft/util/decorators.py +++ b/packages/syft/src/syft/util/decorators.py @@ -2,6 +2,10 @@ from collections.abc import Callable import functools from typing import Any +import warnings + +# relative +from ..service.response import SyftError def singleton(cls: Any) -> Callable: @@ -46,3 +50,25 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: return previous_instances[cls].get("instance") return wrapper + + +def deprecated( + reason: str = "This function is deprecated and may be removed in the future.", + return_syfterror: bool = False, +) -> Callable: + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args: list, **kwargs: dict) -> Any: + message = f"{func.__qualname__} is deprecated: {reason}" + if return_syfterror: + return SyftError(message=message) + warnings.warn( + message, + category=DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/packages/syft/src/syft/util/notebook_ui/components/sync.py b/packages/syft/src/syft/util/notebook_ui/components/sync.py index 215cca934fe..4fdd0adf1b3 100644 --- a/packages/syft/src/syft/util/notebook_ui/components/sync.py +++ b/packages/syft/src/syft/util/notebook_ui/components/sync.py @@ -1,4 +1,5 @@ # stdlib +import datetime from typing import Any # third party @@ -9,6 +10,10 @@ from ....service.code.user_code import UserCode from ....service.job.job_stash import Job from ....service.request.request import Request +from ....service.response import SyftError +from ....service.user.user import UserView +from ....types.datetime import DateTime +from ....types.datetime import format_timedelta_human_readable from ....types.syft_object import SYFT_OBJECT_VERSION_1 from ....types.syft_object import SyftObject from ..icons import Icon @@ -101,6 +106,43 @@ def get_status_str(self) -> str: return status.value return "" # type: ignore + def get_updated_by(self) -> str: + # TODO replace with centralized SyftObject created/updated by attribute + if isinstance(self.object, Request): + email = self.object.requesting_user_email + if email is not None: + return f"Requested by {email}" + + user_view: UserView | SyftError | None = None + if isinstance(self.object, UserCode): + user_view = self.object.user + + if isinstance(user_view, UserView): + return f"Created by {user_view.email}" + return "" + + def get_updated_delta_str(self) -> str: + # TODO replace with centralized SyftObject created/updated by attribute + if isinstance(self.object, Job): + # NOTE Job is not using DateTime for creation_time, so we need to handle it separately + time_str = self.object.creation_time + if time_str is not None: + t = datetime.datetime.fromisoformat(time_str) + delta = datetime.datetime.now(datetime.timezone.utc) - t + return f"{format_timedelta_human_readable(delta)} ago" + + dt: DateTime | None = None + if isinstance(self.object, Request): + dt = self.object.request_time + if isinstance(self.object, UserCode): + dt = self.object.submit_time + if dt is not None: + delta = DateTime.now().timedelta(dt) + delta_str = format_timedelta_human_readable(delta) + return f"{delta_str} ago" + + return "" + def to_html(self) -> str: type_html = TypeLabel(object=self.object).to_html() @@ -110,10 +152,12 @@ def to_html(self) -> str: copy_text=str(self.object.id.id), max_width=60 ).to_html() - updated_delta_str = "29m ago" - updated_by = "john@doe.org" + updated_delta_str = self.get_updated_delta_str() + updated_by = self.get_updated_by() status_str = self.get_status_str() - status_seperator = " β€’ " if len(status_str) else "" + status_row = " β€’ ".join( + s for s in [status_str, updated_by, updated_delta_str] if s + ) summary_html = f"""
@@ -123,7 +167,7 @@ def to_html(self) -> str:
- {status_str}{status_seperator}Updated by {updated_by} {updated_delta_str} + {status_row}
""" # noqa: E501 diff --git a/packages/syft/src/syft/util/util.py b/packages/syft/src/syft/util/util.py index fd2c4dc4074..b0affa2b1a0 100644 --- a/packages/syft/src/syft/util/util.py +++ b/packages/syft/src/syft/util/util.py @@ -19,6 +19,7 @@ import os from pathlib import Path import platform +import random import re from secrets import randbelow import socket @@ -309,7 +310,11 @@ def print_dynamic_log( return (finish, success) -def find_available_port(host: str, port: int, search: bool = False) -> int: +def find_available_port( + host: str, port: int | None = None, search: bool = False +) -> int: + if port is None: + port = random.randint(1500, 65000) # nosec port_available = False while not port_available: try: @@ -324,6 +329,7 @@ def find_available_port(host: str, port: int, search: bool = False) -> int: port += 1 else: break + sock.close() except Exception as e: print(f"Failed to check port {port}. {e}") @@ -338,6 +344,18 @@ def find_available_port(host: str, port: int, search: bool = False) -> int: return port +def get_random_available_port() -> int: + """Retrieve a random available port number from the host OS. + + Returns + ------- + int: Available port number. + """ + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as soc: + soc.bind(("localhost", 0)) + return soc.getsockname()[1] + + def get_loaded_syft() -> ModuleType: return sys.modules[__name__.split(".")[0]] diff --git a/packages/syft/tests/syft/project/project_test.py b/packages/syft/tests/syft/project/project_test.py index 9b2c8ce92f3..c186f2f35fa 100644 --- a/packages/syft/tests/syft/project/project_test.py +++ b/packages/syft/tests/syft/project/project_test.py @@ -23,7 +23,7 @@ def test_project_creation(worker): name="My Cool Project", description="My Cool Description", members=[ds_client] ) - project = new_project.start() + project = new_project.send() assert isinstance(project, Project) assert new_project.id == project.id @@ -47,7 +47,7 @@ def test_error_data_owner_project_creation(worker): name="My Cool Project", description="My Cool Description", members=[root_client] ) - project = new_project.start() + project = new_project.send() assert isinstance(project, sy.SyftError) assert project.message == "Only Data Scientists can create projects" @@ -96,7 +96,7 @@ def test_project_serde(worker): name="My Cool Project", description="My Cool Description", members=[root_client] ) - project = new_project.start() + project = new_project.send() ser_data = sy.serialize(project, to_bytes=True) assert isinstance(ser_data, bytes) diff --git a/packages/syft/tests/syft/service/jobs/job_stash_test.py b/packages/syft/tests/syft/service/jobs/job_stash_test.py index 9d3f5a964aa..271634e1f56 100644 --- a/packages/syft/tests/syft/service/jobs/job_stash_test.py +++ b/packages/syft/tests/syft/service/jobs/job_stash_test.py @@ -1,6 +1,7 @@ # stdlib from datetime import datetime from datetime import timedelta +from datetime import timezone # third party import pytest @@ -33,7 +34,7 @@ def test_eta_string(current_iter, n_iters, status, creation_time_delta, expected node_uid=UID(), n_iters=n_iters, current_iter=current_iter, - creation_time=(datetime.now() - creation_time_delta).isoformat(), + creation_time=(datetime.now(tz=timezone.utc) - creation_time_delta).isoformat(), status=status, ) diff --git a/packages/syft/tests/syft/zmq_queue_test.py b/packages/syft/tests/syft/zmq_queue_test.py index 9b22ac7d260..8c5b8dedebe 100644 --- a/packages/syft/tests/syft/zmq_queue_test.py +++ b/packages/syft/tests/syft/zmq_queue_test.py @@ -21,9 +21,7 @@ from syft.service.response import SyftError from syft.service.response import SyftSuccess from syft.util.util import get_queue_address - -# relative -from ..utils.random_port import get_random_port +from syft.util.util import get_random_available_port @pytest.fixture @@ -118,7 +116,7 @@ def handle_message(message: bytes, *args, **kwargs): @pytest.fixture def producer(): - pub_port = get_random_port() + pub_port = get_random_available_port() QueueName = token_hex(8) # Create a producer diff --git a/packages/syft/tests/utils/random_port.py b/packages/syft/tests/utils/random_port.py deleted file mode 100644 index c3370694afb..00000000000 --- a/packages/syft/tests/utils/random_port.py +++ /dev/null @@ -1,8 +0,0 @@ -# stdlib -import socket - - -def get_random_port(): - soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - soc.bind(("", 0)) - return soc.getsockname()[1] diff --git a/scripts/aa_demo/update_domain.sh b/scripts/aa_demo/update_domain.sh deleted file mode 100644 index 6efa4106b23..00000000000 --- a/scripts/aa_demo/update_domain.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -# $1 domain ip -# $2 dataset url -# install syft in dev mode -pip install -U -e packages/syft - -# get domain name -NODE_NAME="$(sudo docker ps --format '{{.Names}}' | grep "celery" |rev | cut -c 16- | rev)" -echo "Domain Name: ${NODE_NAME}" -echo "Nuking Domain .. >:)"; - -# destroy current domain -hagrid land all -echo "Launching Domain .. hahaha >:)"; - -# re-launch domain -hagrid launch ${NODE_NAME} to docker:80 --dev --build-src="model_training_tests" - -# wait for domain to be up -hagrid check --timeout=120 - -echo "Domain lauch succeeded." -echo "Starting to upload dataset" - -# upload dataset -python scripts/aa_demo/upload_dataset.py $1 $2 - -echo "Upload dataset script complete." diff --git a/scripts/hagrid_hash b/scripts/hagrid_hash deleted file mode 100644 index 715b59990cc..00000000000 --- a/scripts/hagrid_hash +++ /dev/null @@ -1 +0,0 @@ -56f89d45a711a6bf79a460fc8cd4ae20 diff --git a/scripts/staging.json b/scripts/staging.json deleted file mode 100644 index f3b648cff6f..00000000000 --- a/scripts/staging.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain-staging-dev": { - "name": "domain-staging-dev", - "node_type": "domain", - "ip": "40.78.86.246", - "metadata_endpoint": "/api/v1/new/metadata", - "branch": "dev", - "commit_hash": "6bd3bea303107f8eef9ad0e43c85006161835fc9" - } -} diff --git a/scripts/staging.py b/scripts/staging.py deleted file mode 100644 index e158c72b30d..00000000000 --- a/scripts/staging.py +++ /dev/null @@ -1,201 +0,0 @@ -# stdlib -import json -import os -import subprocess -from typing import Any - -# third party -import git -import requests - -DEV_MODE = False -KEY = None -JSON_DATA = os.path.dirname(__file__) + "/staging.json" - - -def run_hagrid(node: dict) -> int: - name = node["name"] - node_type = node["node_type"] - ip = node["ip"] - branch = node["branch"] - cmd = ( - f"hagrid launch {name} {node_type} to {ip} --username=azureuser --auth-type=key " - f"--key-path={KEY} --repo=OpenMined/PySyft --branch={branch} --verbose" - ) - watch_shell(cmd) - - -def watch_shell(command: str) -> None: - process = subprocess.Popen( - command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT - ) # nosec - while True: - output = process.stdout.readline().decode() - if output == "" and process.poll() is not None: - break - if output: - print(output.strip()) - rc = process.poll() - return rc - - -def shell(command: str) -> str: - try: - output = subprocess.check_output( # nosec - command, shell=True, stderr=subprocess.STDOUT - ) - except Exception: - output = b"" - return output.decode("utf-8").strip() - - -def metadata_url(node: dict) -> str: - ip = node["ip"] - endpoint = node["metadata_endpoint"] - return f"http://{ip}{endpoint}" - - -def check_metadata(node: dict) -> dict | None: - try: - res = requests.get(metadata_url(node)) - if res.status_code != 200: - print(f"Got status_code: {res.status_code}") - metadata = res.json() - name = node["name"] - print(f"{name} syft_version: ", metadata["syft_version"]) - return metadata - except Exception as e: - print(f"Failed to get metadata. {e}") - return None - - -def process_node(node: dict[str, Any]) -> tuple[bool, str]: - repo_hash = get_repo_checkout(node) - metadata = check_metadata(node) - hash_string = check_remote_hash(node) - redeploy = False - if metadata is None or hash_string is None: - print(f"redeploy because metadata: {metadata} and remote hash: {hash_string}") - redeploy = True - - if hash_string is not None and repo_hash != hash_string: - print("repo_hash", len(repo_hash), type(repo_hash)) - print("hash_string", len(hash_string), type(hash_string)) - print( - f"redeploy because repo_hash: {repo_hash} != remote hash_string: {hash_string}" - ) - redeploy = True - - if redeploy: - print("πŸ”§ Redeploying with HAGrid") - run_hagrid(node) - - hash_string = check_remote_hash(node) - if hash_string is None: - print(f"Cant get hash: {hash_string}") - - if hash_string is not None and hash_string != repo_hash: - print( - f"Hash doesnt match repo_hash: {repo_hash} != remote hash_string {hash_string}" - ) - - metadata = check_metadata(node) - if metadata is None: - print(f"Cant get metadata: {metadata}") - - if metadata and hash_string == repo_hash: - return True, repo_hash - return False, repo_hash - - -def get_repo_checkout(node: dict) -> str: - try: - branch = node["branch"] - repo_path = f"/tmp/{branch}/PySyft" - if not os.path.exists(repo_path): - os.makedirs(repo_path, exist_ok=True) - repo = git.Repo.clone_from( - "https://github.com/OpenMined/pysyft", - repo_path, - single_branch=True, - b=branch, - ) - else: - repo = git.Repo(repo_path) - if repo.is_dirty(): - repo.git.reset("--hard") - repo.git.checkout(branch) - repo.remotes.origin.pull() - sha = repo.head.commit.hexsha - return sha - except Exception as e: - print(f"Failed to get branch HEAD commit hash. {e}") - raise e - - -def run_remote_shell(node: dict, cmd: str) -> str | None: - try: - ip = node["ip"] - ssh_cmd = ( - f"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -i {KEY} azureuser@{ip}" - ) - shell_cmd = f'{ssh_cmd} "{cmd}"' - print("Running:", shell_cmd) - return shell(shell_cmd) - except Exception: - print("Failed to run ssh command: {}") - return None - - -def check_remote_hash(node: dict) -> str | None: - cmd = "sudo runuser -l om -c 'cd /home/om/PySyft && git rev-parse HEAD'" - return run_remote_shell(node, cmd) - - -def check_staging() -> None: - nodes = load_staging_data(JSON_DATA) - for name, node in nodes.items(): - print(f"Processing {name}") - good = False - try: - good, updated_hash = process_node(node=node) - node["commit_hash"] = updated_hash - nodes[name] = node - save_staging_data(JSON_DATA, nodes) - except Exception as e: - print(f"Failed to process node: {name}. {e}") - emoji = "βœ…" if good else "❌" - print(f"{emoji} Node {name}") - - -def load_staging_data(path: str) -> dict[str, dict]: - with open(path) as f: - return json.loads(f.read()) - - -def save_staging_data(path: str, data: dict[str, dict]) -> None: - print("Saving changes to file", path) - with open(path, "w") as f: - f.write(f"{json.dumps(data)}") - - -if __name__ == "__main__": - # stdlib - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument("--dev", action="store_true", help="Dev Mode") - parser.add_argument("--private-key", help="Dev Mode") - - args = parser.parse_args() - if args.dev: - DEV_MODE = True - if args.private_key: - path = os.path.expanduser(args.private_key) - if os.path.exists(path): - KEY = path - if KEY is None: - raise Exception("--private-key required") - print("DEV MODE", DEV_MODE) - - check_staging() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f6ccf94f32c..9152038b1f7 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -51,7 +51,6 @@ def full_low_worker(n_consumers: int = 3, create_producer: bool = True) -> Worke n_consumers=n_consumers, create_producer=create_producer, queue_port=None, - in_memory_workers=True, local_db=False, thread_workers=False, ) @@ -72,7 +71,6 @@ def full_high_worker(n_consumers: int = 3, create_producer: bool = True) -> Work n_consumers=n_consumers, create_producer=create_producer, queue_port=None, - in_memory_workers=True, local_db=False, thread_workers=False, ) diff --git a/tests/integration/container_workload/pool_image_test.py b/tests/integration/container_workload/pool_image_test.py index d8cb59f8f1f..96613240660 100644 --- a/tests/integration/container_workload/pool_image_test.py +++ b/tests/integration/container_workload/pool_image_test.py @@ -1,38 +1,70 @@ # stdlib import os -from time import sleep # third party from faker import Faker import numpy as np import pytest +import requests # syft absolute import syft as sy from syft.client.domain_client import DomainClient from syft.custom_worker.config import DockerWorkerConfig -from syft.node.node import get_default_worker_tag_by_env from syft.service.request.request import Request from syft.service.response import SyftSuccess from syft.service.worker.worker_image import SyftWorkerImage from syft.service.worker.worker_pool import SyftWorker from syft.service.worker.worker_pool import WorkerPool -SYFT_BASE_TAG = get_default_worker_tag_by_env() -hagrid_flags = os.getenv("HAGRID_FLAGS") -if hagrid_flags: - SYFT_BASE_TAG = get_default_worker_tag_by_env(dev_mode=True) +registry = os.getenv("SYFT_BASE_IMAGE_REGISTRY", "docker.io") +repo = "openmined/grid-backend" + +if "k3d" in registry: + res = requests.get(url=f"http://{registry}/v2/{repo}/tags/list") + tag = res.json()["tags"][0] +else: + tag = sy.__version__ + +external_registry = os.getenv("EXTERNAL_REGISTRY", registry) +external_registry_username = os.getenv("EXTERNAL_REGISTRY_USERNAME", None) +external_registry_password = os.getenv("EXTERNAL_REGISTRY_PASSWORD", None) + + +@pytest.fixture +def external_registry_uid(domain_1_port): + domain_client: DomainClient = sy.login( + port=domain_1_port, email="info@openmined.org", password="changethis" + ) + image_registry_list = domain_client.api.services.image_registry.get_all() + if len(image_registry_list) > 1: + raise Exception("Only one registry should be present for testing") + + elif len(image_registry_list) == 1: + assert ( + image_registry_list[0].url == external_registry + ), "External registry different from the one set in the environment variable" + return image_registry_list[0].id + else: + registry_add_result = domain_client.api.services.image_registry.add( + external_registry + ) + + assert isinstance(registry_add_result, sy.SyftSuccess), str(registry_add_result) + + image_registry_list = domain_client.api.services.image_registry.get_all() + return image_registry_list[0].id @pytest.mark.container_workload -def test_image_build(domain_1_port) -> None: +def test_image_build(domain_1_port, external_registry_uid) -> None: domain_client: DomainClient = sy.login( port=domain_1_port, email="info@openmined.org", password="changethis" ) - # Submit Docker Worker Config + # Submit Docker Worker Config. docker_config_rl = f""" - FROM openmined/grid-backend:{SYFT_BASE_TAG} + FROM {registry}/{repo}:{tag} RUN pip install recordlinkage """ docker_config = DockerWorkerConfig(dockerfile=docker_config_rl) @@ -49,12 +81,11 @@ def test_image_build(domain_1_port) -> None: assert not isinstance(workerimage, sy.SyftError) # Build docker image - tag_version = sy.UID().short() - docker_tag = f"openmined/custom-worker-rl:{tag_version}" + docker_tag = "openmined/custom-worker-rl:latest" docker_build_result = domain_client.api.services.worker_image.build( image_uid=workerimage.id, tag=docker_tag, - pull=False, + registry_uid=external_registry_uid, ) assert isinstance(docker_build_result, SyftSuccess) @@ -67,18 +98,9 @@ def test_image_build(domain_1_port) -> None: assert workerimage.image_identifier.repo_with_tag == docker_tag assert workerimage.image_hash is not None - # Delete image - delete_result = domain_client.api.services.worker_image.remove(uid=workerimage.id) - assert isinstance(delete_result, sy.SyftSuccess) - - # Validate the image is successfully deleted - assert len(domain_client.images.get_all()) == 1 - workerimage = domain_client.images.get_all()[0] - assert workerimage.config != docker_config - @pytest.mark.container_workload -def test_pool_launch(domain_1_port) -> None: +def test_pool_launch(domain_1_port, external_registry_uid) -> None: domain_client: DomainClient = sy.login( port=domain_1_port, email="info@openmined.org", password="changethis" ) @@ -86,7 +108,7 @@ def test_pool_launch(domain_1_port) -> None: # Submit Docker Worker Config docker_config_opendp = f""" - FROM openmined/grid-backend:{SYFT_BASE_TAG} + FROM {registry}/{repo}:{tag} RUN pip install opendp """ docker_config = DockerWorkerConfig(dockerfile=docker_config_opendp) @@ -103,18 +125,24 @@ def test_pool_launch(domain_1_port) -> None: assert not worker_image.is_built # Build docker image - tag_version = sy.UID().short() - docker_tag = f"openmined/custom-worker-opendp:{tag_version}" + docker_tag = "openmined/custom-worker-opendp:latest" docker_build_result = domain_client.api.services.worker_image.build( image_uid=worker_image.id, tag=docker_tag, - pull=False, + registry_uid=external_registry_uid, ) assert isinstance(docker_build_result, SyftSuccess) + # Push Image to External registry + push_result = domain_client.api.services.worker_image.push( + worker_image.id, + username=external_registry_username, + password=external_registry_password, + ) + assert isinstance(push_result, sy.SyftSuccess), str(push_result) + # Launch a worker pool - pool_version = sy.UID().short() - worker_pool_name = f"custom_worker_pool_ver{pool_version}" + worker_pool_name = "custom-worker-pool-opendp" worker_pool_res = domain_client.api.services.worker_pool.launch( name=worker_pool_name, image_uid=worker_image.id, @@ -156,14 +184,9 @@ def test_pool_launch(domain_1_port) -> None: # TODO: delete the launched pool - # Clean the build images - sleep(10) - delete_result = domain_client.api.services.worker_image.remove(uid=worker_image.id) - assert isinstance(delete_result, sy.SyftSuccess) - @pytest.mark.container_workload -def test_pool_image_creation_job_requests(domain_1_port) -> None: +def test_pool_image_creation_job_requests(domain_1_port, external_registry_uid) -> None: """ Test register ds client, ds requests to create an image and pool creation, do approves, then ds creates a function attached to the worker pool, then creates another @@ -187,21 +210,19 @@ def test_pool_image_creation_job_requests(domain_1_port) -> None: # the DS makes a request to create an image and a pool based on the image docker_config_np = f""" - FROM openmined/grid-backend:{SYFT_BASE_TAG} + FROM {registry}/{repo}:{tag} RUN pip install numpy """ docker_config = DockerWorkerConfig(dockerfile=docker_config_np) - tag_version = sy.UID().short() - docker_tag = f"openmined/custom-worker-np:{tag_version}" - pool_version = sy.UID().short() - worker_pool_name = f"custom_worker_pool_ver{pool_version}" + docker_tag = "openmined/custom-worker-np:latest" + worker_pool_name = "custom-worker-pool-numpy" request = ds_client.api.services.worker_pool.create_image_and_pool_request( pool_name=worker_pool_name, num_workers=1, tag=docker_tag, config=docker_config, reason="I want to do some more cool data science with PySyft and Recordlinkage", - pull_image=False, + registry_uid=external_registry_uid, ) assert isinstance(request, Request) assert len(request.changes) == 2 @@ -224,7 +245,7 @@ def test_pool_image_creation_job_requests(domain_1_port) -> None: worker: SyftWorker = launched_pool.workers[0] assert launched_pool.name in worker.name - assert worker.status.value == "Pending" + assert worker.status.value == "Running" assert worker.healthcheck.value == "βœ…" # assert worker.consumer_state.value == "Idle" assert isinstance(worker.logs, str) @@ -279,8 +300,3 @@ def custom_worker_func(x): assert isinstance(res, sy.SyftSuccess) # TODO: delete the launched pool - - # Clean the build images - sleep(10) - delete_result = domain_client.api.services.worker_image.remove(uid=built_image.id) - assert isinstance(delete_result, sy.SyftSuccess) diff --git a/tests/integration/local/request_multiple_nodes_test.py b/tests/integration/local/request_multiple_nodes_test.py index 601988673dc..e81f75b57d6 100644 --- a/tests/integration/local/request_multiple_nodes_test.py +++ b/tests/integration/local/request_multiple_nodes_test.py @@ -21,7 +21,6 @@ def node_1(): local_db=True, create_producer=True, n_consumers=1, - in_memory_workers=True, queue_port=None, ) yield node @@ -39,7 +38,6 @@ def node_2(): local_db=True, create_producer=True, n_consumers=1, - in_memory_workers=True, queue_port=None, ) yield node diff --git a/tests/integration/local/syft_function_test.py b/tests/integration/local/syft_function_test.py index 6ca60f3b90d..8cc85cce4e2 100644 --- a/tests/integration/local/syft_function_test.py +++ b/tests/integration/local/syft_function_test.py @@ -23,7 +23,6 @@ def node(): n_consumers=3, create_producer=True, queue_port=None, - in_memory_workers=True, local_db=False, ) # startup code here diff --git a/tests/integration/local/twin_api_sync_test.py b/tests/integration/local/twin_api_sync_test.py index d39066ade9a..e09c82001d1 100644 --- a/tests/integration/local/twin_api_sync_test.py +++ b/tests/integration/local/twin_api_sync_test.py @@ -111,7 +111,7 @@ def compute(query): # verify that ds cannot access private job assert client_low_ds.api.services.job.get(private_job_id) is None - assert low_client.api.services.job.get(private_job_id) is not None + assert low_client.api.services.job.get(private_job_id) is None # we only sync the mock function, we never sync the private function to the low side mock_res = low_client.api.services.testapi.query.mock() diff --git a/tox.ini b/tox.ini index 41e6f168c95..c79fa513cb1 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,6 @@ envlist = dev.k8s.cleanup dev.k8s.destroy dev.k8s.destroyall - hagrid.publish lint stack.test.integration syft.docs @@ -69,16 +68,6 @@ allowlist_externals = commands = bash -c 'uv pip list || pip list' -[testenv:hagrid] -deps = - -e{toxinidir}/packages/hagrid[dev] -changedir = {toxinidir}/packages/hagrid -description = Syft -allowlist_externals = - bash -commands = - bash -c 'uv pip list || pip list' - [testenv:syftcli] deps = -e{toxinidir}/packages/syftcli[dev] @@ -98,14 +87,6 @@ commands = python -c 'from shutil import rmtree; rmtree("build", True); rmtree("dist", True)' python -m build . -[testenv:hagrid.publish] -changedir = {toxinidir}/packages/hagrid -description = Build and Publish Hagrid Wheel -deps = - build -commands = - python -c 'from shutil import rmtree; rmtree("build", True); rmtree("dist", True)' - python -m build . [testenv:syftcli.publish] changedir = {toxinidir}/packages/syftcli @@ -179,8 +160,6 @@ commands = [testenv:frontend.test.unit] description = Frontend Unit Tests -deps = - {[testenv:hagrid]deps} allowlist_externals = docker bash @@ -201,145 +180,6 @@ commands = docker run -t ui-test; \ fi' -[testenv:frontend.test.e2e] -description = Frontend Unit Tests -deps = - {[testenv:hagrid]deps} -allowlist_externals = - docker - bash - pnpm - sleep -passenv=HOME, USER -changedir = {toxinidir}/packages/grid/frontend -setenv = - HAGRID_FLAGS = {env:HAGRID_FLAGS:--tag=local --test} - ENABLE_SIGNUP=True -commands = - bash ./scripts/check_pnpm.sh - - bash -c "echo Running with HAGRID_FLAGS=$HAGRID_FLAGS; date" - - ; install hagrid - bash -c 'if [[ "$HAGRID_FLAGS" == *"local"* ]]; then \ - uv pip install -e "../../hagrid"; \ - else \ - uv pip install --force hagrid; \ - fi' - - ; fix windows encoding - - chcp 65001 - - ; check docker versions - bash -c "docker --version" - bash -c "docker compose version" - - ; reset volumes and create nodes - bash -c "echo Starting Nodes; date" - bash -c "docker rm -f $(docker ps -a -q) || true" - bash -c 'docker volume rm -f $(docker volume ls -q --filter "label=orgs.openmined.syft") || true' - - bash -c 'HAGRID_ART=$HAGRID_ART hagrid launch test_domain_1 domain to docker:9081 $HAGRID_FLAGS --enable-signup --no-health-checks --verbose --no-warnings' - - bash -c '(docker logs test_domain_1-frontend-1 -f &) | grep -q -E "Network:\s+https?://[a-zA-Z0-9.-]+:[0-9]+/" || true' - bash -c '(docker logs test_domain_1-backend-1 -f &) | grep -q "Application startup complete" || true' - - pnpm install - pnpm dlx playwright@1.36.1 install --with-deps - pnpm exec playwright install - pnpm test:e2e - - ; shutdown - bash -c "echo Killing Nodes; date" - bash -c 'HAGRID_ART=false hagrid land all --force' - bash -c 'docker volume rm -f $(docker volume ls -q --filter "label=orgs.openmined.syft") || true' - - -[testenv:stack.test.integration] -description = Integration Tests for Core Stack -deps = - {[testenv:syft]deps} - {[testenv:hagrid]deps} - pytest -changedir = {toxinidir} -allowlist_externals = - docker - grep - sleep - bash - chcp -passenv=HOME, USER, AZURE_BLOB_STORAGE_KEY -setenv = - HAGRID_FLAGS = {env:HAGRID_FLAGS:--tag=local --dev} - EMULATION = {env:EMULATION:false} - HAGRID_ART = false - PYTHONIOENCODING = utf-8 - PYTEST_MODULES = {env:PYTEST_MODULES:frontend container_workload network local} -commands = - bash -c "whoami; id;" - - bash -c "echo Running with HAGRID_FLAGS=$HAGRID_FLAGS EMULATION=$EMULATION PYTEST_MODULES=$PYTEST_MODULES; date" - - ; install syft and hagrid - bash -c 'if [[ "$HAGRID_FLAGS" == *"latest"* ]]; then \ - echo "Installing latest syft and hagrid"; \ - uv pip install --force hagrid syft; \ - elif [[ "$HAGRID_FLAGS" == *"beta"* ]]; then \ - echo "Installing beta syft and hagrid"; \ - uv pip install --force hagrid; \ - uv pip install --force -U --pre syft; \ - else \ - echo "Using local syft and hagrid"; \ - fi' - - ; fix windows encoding - - chcp 65001 - - ; check docker versions - bash -c "docker --version" - bash -c "docker compose version" - - ; reset volumes and create nodes - bash -c "echo Starting Nodes; date" - bash -c 'docker rm -f $(docker ps -a -q --filter "label=orgs.openmined.syft") || true' - bash -c 'docker volume rm -f $(docker volume ls -q --filter "label=orgs.openmined.syft") || true' - - python -c 'import syft as sy; sy.stage_protocol_changes()' - - ; Make sure that pacakge-cache is owned by the current user - ; instead of docker creating it as root - bash -c 'mkdir -p packages/grid/data/package-cache' - - bash -c 'HAGRID_ART=$HAGRID_ART hagrid launch test-gateway-1 gateway to docker:9081 $HAGRID_FLAGS --no-health-checks --verbose --no-warnings --build' - bash -c 'HAGRID_ART=$HAGRID_ART hagrid launch test-domain-1 domain to docker:9082 $HAGRID_FLAGS --no-health-checks --enable-signup --verbose --no-warnings --build' - ; bash -c 'HAGRID_ART=$HAGRID_ART hagrid launch test-domain-2 domain to docker:9083 --headless $HAGRID_FLAGS --enable-signup --no-health-checks --verbose --no-warnings --build' - - ; wait for nodes to start - docker ps - bash -c "echo Waiting for Nodes; date" - bash -c '(docker logs test-domain-1-frontend-1 -f &) | grep -q -E "Network:\s+https?://[a-zA-Z0-9.-]+:[0-9]+/" || true' - bash -c '(docker logs test-domain-1-backend-1 -f &) | grep -q "Application startup complete" || true' - ; bash -c '(docker logs test_domain_2-backend-1 -f &) | grep -q "Application startup complete" || true' - bash -c '(docker logs test-gateway-1-backend-1 -f &) | grep -q "Application startup complete" || true' - - bash -c '\ - PYTEST_MODULES=($PYTEST_MODULES); \ - for i in "${PYTEST_MODULES[@]}"; do \ - echo "Starting test for $i"; date; \ - pytest tests/integration -m $i -vvvv -p no:randomly -p no:benchmark -o log_cli=True --capture=no; \ - return=$?; \ - echo "Finished $i"; \ - date; \ - if [[ $return -ne 0 ]]; then \ - exit $return; \ - fi; \ - done' - - ; shutdown - bash -c "echo Killing Nodes; date" - bash -c 'HAGRID_ART=false hagrid land all --force' - bash -c 'docker rm -f $(docker ps -a -q --filter "label=orgs.openmined.syft") || true' - bash -c 'docker volume rm -f $(docker volume ls -q --filter "label=orgs.openmined.syft") || true' [testenv:syft.docs] @@ -347,7 +187,6 @@ description = Build Docs for Syft changedir = {toxinidir}/docs deps = {[testenv:syft]deps} - {[testenv:hagrid]deps} -r {toxinidir}/docs/requirements.txt allowlist_externals = make @@ -368,7 +207,6 @@ commands = description = Jupyter Notebook with Editable Syft deps = {[testenv:syft]deps} - {[testenv:hagrid]deps} jupyter jupyterlab commands = @@ -395,7 +233,6 @@ description = Security Checks for Syft changedir = {toxinidir}/packages/syft deps = {[testenv:syft]deps} - {[testenv:hagrid]deps} commands = bandit -r src # ansible 8.4.0 @@ -406,7 +243,6 @@ commands = description = Syft Unit Tests deps = {[testenv:syft]deps} - {[testenv:hagrid]deps} allowlist_externals = bash uv @@ -421,7 +257,6 @@ commands = description = Syft Notebook Tests deps = -e{toxinidir}/packages/syft[dev,data_science] - {[testenv:hagrid]deps} nbmake changedir = {toxinidir}/notebooks allowlist_externals = @@ -490,20 +325,18 @@ commands = description = Stack Notebook Tests deps = {[testenv:syft]deps} - {[testenv:hagrid]deps} nbmake changedir = {toxinidir}/notebooks allowlist_externals = bash setenv = - ORCHESTRA_DEPLOYMENT_TYPE = {env:ORCHESTRA_DEPLOYMENT_TYPE:k8s} + ORCHESTRA_DEPLOYMENT_TYPE = {env:ORCHESTRA_DEPLOYMENT_TYPE:remote} DEV_MODE = {env:DEV_MODE:True} TEST_NOTEBOOK_PATHS = {env:TEST_NOTEBOOK_PATHS:api/0.8} ENABLE_SIGNUP=True commands = # Volume cleanup - bash -c 'hagrid land all --force --prune-vol || true' bash -c 'docker volume rm -f $(docker volume ls -q --filter "label=orgs.openmined.syft") || true' bash -c 'docker volume rm -f $(docker volume ls -q --filter "label=com.docker.volume.anonymous") || true' bash -c 'docker network rm -f $(docker network ls -q --filter "label=orgs.openmined.syft") || true' @@ -519,7 +352,6 @@ commands = ; pytest -x --nbmake --nbmake-timeout=1000 tutorials -p no:randomly -vvvv ; pytest -x --nbmake --nbmake-timeout=1000 tutorials/pandas-cookbook -p no:randomly -vvvv - bash -c 'hagrid land all --force --prune-vol' bash -c 'docker volume rm -f $(docker volume ls -q --filter "label=orgs.openmined.syft") || true' bash -c 'docker volume rm -f $(docker volume ls -q --filter "label=com.docker.volume.anonymous") || true' bash -c 'docker network rm -f $(docker network ls -q --filter "label=orgs.openmined.syft") || true' @@ -528,7 +360,6 @@ commands = description = Stack VM Tests deps = {[testenv:syft]deps} - {[testenv:hagrid]deps} nbmake allowlist_externals = cd @@ -578,7 +409,6 @@ commands = description = Stack podman Tests for Rhel & Centos deps = {[testenv:syft]deps} - {[testenv:hagrid]deps} nbmake allowlist_externals = cd @@ -611,7 +441,6 @@ commands = description = Generate Types for Frontend deps = {[testenv:syft]deps} - {[testenv:hagrid]deps} allowlist_externals = cd bash @@ -636,16 +465,43 @@ commands = python_version = 3.12 disable_error_code = attr-defined, valid-type, no-untyped-call, arg-type +[testenv:syft.test.integration] +description = Integration Tests for Syft Stack +basepython = python3 +deps = + {[testenv:syft]deps} +changedir = {toxinidir} +passenv=HOME, USER +allowlist_externals = + bash +setenv = + PYTEST_MODULES = {env:PYTEST_MODULES:local_node} + ASSOCIATION_REQUEST_AUTO_APPROVAL = {env:ASSOCIATION_REQUEST_AUTO_APPROVAL:true} + PYTEST_FLAGS = {env:PYTEST_FLAGS:--ignore=tests/integration/local/gateway_local_test.py --ignore=tests/integration/local/job_test.py} +commands = + python -c 'import syft as sy; sy.stage_protocol_changes()' + + # Run Integration Tests + bash -c '\ + PYTEST_MODULES=($PYTEST_MODULES); \ + for i in "${PYTEST_MODULES[@]}"; do \ + echo "Starting test for $i"; date; \ + pytest tests/integration -m $i -vvvv -p no:randomly -p no:benchmark -o log_cli=True --capture=no $PYTEST_FLAGS; \ + return=$?; \ + echo "Finished $i"; \ + date; \ + if [[ $return -ne 0 ]]; then \ + exit $return; \ + fi; \ + done' [testenv:stack.test.integration.k8s] -description = Integration Tests for Core Stack +description = Integration Tests for Core Stack using K8s basepython = python3 deps = {[testenv:syft]deps} - {[testenv:hagrid]deps} - nbmake changedir = {toxinidir} -passenv=HOME, USER +passenv=HOME, USER, AZURE_BLOB_STORAGE_KEY allowlist_externals = devspace kubectl @@ -657,41 +513,39 @@ allowlist_externals = echo tox setenv = - ORCHESTRA_DEPLOYMENT_TYPE = {env:ORCHESTRA_DEPLOYMENT_TYPE:k8s} NODE_PORT = {env:NODE_PORT:9082} GITHUB_CI = {env:GITHUB_CI:false} - PYTEST_MODULES = {env:PYTEST_MODULES:frontend container_workload local} - SYFT_BASE_IMAGE_REGISTRY = {env:SYFT_BASE_IMAGE_REGISTRY:k3d-registry.localhost:5800} + PYTEST_MODULES = {env:PYTEST_MODULES:frontend network container_workload} + DOMAIN_CLUSTER_NAME = {env:DOMAIN_CLUSTER_NAME:test-domain-1} + GATEWAY_CLUSTER_NAME = {env:GATEWAY_CLUSTER_NAME:test-gateway-1} ASSOCIATION_REQUEST_AUTO_APPROVAL = {env:ASSOCIATION_REQUEST_AUTO_APPROVAL:true} + SYFT_BASE_IMAGE_REGISTRY = {env:SYFT_BASE_IMAGE_REGISTRY:k3d-registry.localhost:5800} commands = bash -c "echo Running with GITHUB_CI=$GITHUB_CI; date" python -c 'import syft as sy; sy.stage_protocol_changes()' k3d version - # Since cluster name cannot have underscore and environment variable cannot have hyphen - # we are passing a grouped name for node names - # bash -c "docker rm $(docker ps -aq) --force || true" - # Deleting current cluster - bash -c "k3d cluster delete testgateway1 || true" - bash -c "k3d cluster delete testdomain1 || true" + # Deleting Old Cluster + bash -c "k3d cluster delete ${DOMAIN_CLUSTER_NAME} || true" + bash -c "k3d cluster delete ${GATEWAY_CLUSTER_NAME} || true" # Deleting registry & volumes bash -c "k3d registry delete k3d-registry.localhost || true" - bash -c "docker volume rm k3d-testgateway1-images --force || true" - bash -c "docker volume rm k3d-testdomain1-images --force || true" + bash -c "docker volume rm k3d-${DOMAIN_CLUSTER_NAME}-images --force || true" + bash -c "docker volume rm k3d-${GATEWAY_CLUSTER_NAME}-images --force || true" # Create registry tox -e dev.k8s.registry - # Creating testgateway1 cluster on port 9081 + # Creating test-gateway-1 cluster on port 9081 bash -c '\ - export CLUSTER_NAME=testgateway1 CLUSTER_HTTP_PORT=9081 DEVSPACE_PROFILE=gateway && \ + export CLUSTER_NAME=${GATEWAY_CLUSTER_NAME} CLUSTER_HTTP_PORT=9081 DEVSPACE_PROFILE=gateway && \ tox -e dev.k8s.start && \ tox -e dev.k8s.deploy' - # Creating testdomain1 cluster on port 9082 + # Creating test-domain-1 cluster on port 9082 bash -c '\ - export CLUSTER_NAME=testdomain1 CLUSTER_HTTP_PORT=9082 && \ + export CLUSTER_NAME=${DOMAIN_CLUSTER_NAME} CLUSTER_HTTP_PORT=9082 && \ tox -e dev.k8s.start && \ tox -e dev.k8s.deploy' @@ -703,65 +557,115 @@ commands = sleep 30 - # wait for front end - bash packages/grid/scripts/wait_for.sh service frontend --context k3d-testdomain1 --namespace syft - bash -c '(kubectl logs service/frontend --context k3d-testdomain1 --namespace syft -f &) | grep -q -E "Network:\s+https?://[a-zA-Z0-9.-]+:[0-9]+/" || true' - # wait for test gateway 1 - bash packages/grid/scripts/wait_for.sh service mongo --context k3d-testgateway1 --namespace syft - bash packages/grid/scripts/wait_for.sh service backend --context k3d-testgateway1 --namespace syft - bash packages/grid/scripts/wait_for.sh service proxy --context k3d-testgateway1 --namespace syft + bash packages/grid/scripts/wait_for.sh service mongo --context k3d-{env:GATEWAY_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service backend --context k3d-{env:GATEWAY_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service proxy --context k3d-{env:GATEWAY_CLUSTER_NAME} --namespace syft # wait for test domain 1 - bash packages/grid/scripts/wait_for.sh service mongo --context k3d-testdomain1 --namespace syft - bash packages/grid/scripts/wait_for.sh service backend --context k3d-testdomain1 --namespace syft - bash packages/grid/scripts/wait_for.sh service proxy --context k3d-testdomain1 --namespace syft - bash packages/grid/scripts/wait_for.sh service seaweedfs --context k3d-testdomain1 --namespace syft + bash packages/grid/scripts/wait_for.sh service mongo --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service backend --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service proxy --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service seaweedfs --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service frontend --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash -c '(kubectl logs service/frontend --context k3d-${DOMAIN_CLUSTER_NAME} --namespace syft -f &) | grep -q -E "Network:\s+https?://[a-zA-Z0-9.-]+:[0-9]+/" || true' # Checking logs generated & startup of test-domain 1 - bash -c '(kubectl logs service/backend --context k3d-testdomain1 --namespace syft -f &) | grep -q "Application startup complete" || true' + bash -c '(kubectl logs service/backend --context k3d-${DOMAIN_CLUSTER_NAME} --namespace syft -f &) | grep -q "Application startup complete" || true' # Checking logs generated & startup of testgateway1 - bash -c '(kubectl logs service/backend --context k3d-testgateway1 --namespace syft -f &) | grep -q "Application startup complete" || true' - - # frontend - bash -c 'if [[ "$PYTEST_MODULES" == *"frontend"* ]]; then \ - echo "Starting frontend"; date; \ - pytest tests/integration -m frontend -p no:randomly -k "test_serves_domain_frontend" --co; \ - pytest tests/integration -m frontend -vvvv -p no:randomly -p no:benchmark -o log_cli=True --capture=no -k "test_serves_domain_frontend"; \ - return=$?; \ - echo "Finished frontend"; date; \ - exit $return; \ + bash -c '(kubectl logs service/backend --context k3d-${GATEWAY_CLUSTER_NAME} --namespace syft -f &) | grep -q "Application startup complete" || true' + + # Run Integration Tests + bash -c '\ + PYTEST_MODULES=($PYTEST_MODULES); \ + for i in "${PYTEST_MODULES[@]}"; do \ + echo "Starting test for $i"; date; \ + pytest tests/integration -m $i -vvvv -p no:randomly -p no:benchmark -o log_cli=True --capture=no; \ + return=$?; \ + echo "Finished $i"; \ + date; \ + if [[ $return -ne 0 ]]; then \ + exit $return; \ + fi; \ + done' + + # deleting clusters created + bash -c "CLUSTER_NAME=${DOMAIN_CLUSTER_NAME} tox -e dev.k8s.destroy || true" + bash -c "CLUSTER_NAME=${GATEWAY_CLUSTER_NAME} tox -e dev.k8s.destroy || true" + bash -c "k3d registry delete k3d-registry.localhost || true" + bash -c "docker volume rm k3d-${DOMAIN_CLUSTER_NAME}-images --force || true" + bash -c "docker volume rm k3d-${GATEWAY_CLUSTER_NAME}-images --force || true" + +[testenv:stack.test.notebook.k8s] +description = Notebook Tests for Core Stack using K8s +basepython = python3 +deps = + {[testenv:syft]deps} + nbmake +changedir = {toxinidir} +passenv=HOME, USER +allowlist_externals = + devspace + kubectl + grep + sleep + bash + k3d + echo + tox +setenv = + ORCHESTRA_DEPLOYMENT_TYPE = {env:ORCHESTRA_DEPLOYMENT_TYPE:remote} + GITHUB_CI = {env:GITHUB_CI:false} + SYFT_BASE_IMAGE_REGISTRY = {env:SYFT_BASE_IMAGE_REGISTRY:k3d-registry.localhost:5800} + DOMAIN_CLUSTER_NAME = {env:DOMAIN_CLUSTER_NAME:test-domain-1} + NODE_PORT = {env:NODE_PORT:8080} +commands = + bash -c "echo Running with GITHUB_CI=$GITHUB_CI; date" + python -c 'import syft as sy; sy.stage_protocol_changes()' + k3d version + + # Deleting Old Cluster + bash -c "k3d cluster delete ${DOMAIN_CLUSTER_NAME} || true" + + # Deleting registry & volumes + bash -c "k3d registry delete k3d-registry.localhost || true" + bash -c "docker volume rm k3d-${DOMAIN_CLUSTER_NAME}-images --force || true" + + # Create registry + tox -e dev.k8s.registry + + + # Creating test-domain-1 cluster on port NODE_PORT + bash -c '\ + export CLUSTER_NAME=${DOMAIN_CLUSTER_NAME} CLUSTER_HTTP_PORT=${NODE_PORT} && \ + tox -e dev.k8s.start && \ + tox -e dev.k8s.deploy' + + # free up build cache after build of images + bash -c 'if [[ "$GITHUB_CI" != "false" ]]; then \ + docker image prune --all --force; \ + docker builder prune --all --force; \ fi' - # Integration + Gateway Connection Tests - # Gateway tests are not run in kuberetes, as currently,it does not have a way to configure - # high/low side warning flag. - bash -c "source ./scripts/get_k8s_secret_ci.sh; \ - pytest tests/integration/network -k 'not test_domain_gateway_user_code' -p no:randomly -vvvv" - - # Shutting down the gateway cluster to free up space, as the - # below code does not require gateway cluster - bash -c "CLUSTER_NAME=testgateway1 tox -e dev.k8s.destroy || true" - bash -c "docker volume rm k3d-testgateway1-images --force || true" - - ; container workload - ; bash -c 'if [[ "$PYTEST_MODULES" == *"container_workload"* ]]; then \ - ; echo "Starting Container Workload test"; date; \ - ; pytest tests/integration -m container_workload -p no:randomly --co; \ - ; pytest tests/integration -m container_workload -vvvv -p no:randomly -p no:benchmark -o log_cli=True --capture=no; \ - ; return=$?; \ - ; echo "Finished container workload"; date; \ - ; exit $return; \ - ; fi' - - bash -c "source ./scripts/get_k8s_secret_ci.sh; \ - pytest -x --nbmake --nbmake-timeout=1000 notebooks/api/0.8 -p no:randomly -k 'not 10-container-images.ipynb' -vvvv" + sleep 30 + + # wait for test-domain-1 + bash packages/grid/scripts/wait_for.sh service mongo --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service backend --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service proxy --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service seaweedfs --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash packages/grid/scripts/wait_for.sh service frontend --context k3d-{env:DOMAIN_CLUSTER_NAME} --namespace syft + bash -c '(kubectl logs service/frontend --context k3d-${DOMAIN_CLUSTER_NAME} --namespace syft -f &) | grep -q -E "Network:\s+https?://[a-zA-Z0-9.-]+:[0-9]+/" || true' + + # Checking logs generated & startup of test-domain 1 + bash -c '(kubectl logs service/backend --context k3d-${DOMAIN_CLUSTER_NAME} --namespace syft -f &) | grep -q "Application startup complete" || true' + + bash -c "pytest -x --nbmake notebooks/api/0.8 -p no:randomly -k 'not 10-container-images.ipynb' -vvvv --nbmake-timeout=1000" # deleting clusters created - bash -c "CLUSTER_NAME=testdomain1 tox -e dev.k8s.destroy || true" + bash -c "CLUSTER_NAME=${DOMAIN_CLUSTER_NAME} tox -e dev.k8s.destroy || true" bash -c "k3d registry delete k3d-registry.localhost || true" - bash -c "docker rm $(docker ps -aq) --force || true" - bash -c "docker volume rm k3d-testdomain1-images --force || true" + bash -c "docker volume rm k3d-${DOMAIN_CLUSTER_NAME}-images --force || true" [testenv:syft.build.helm] @@ -817,7 +721,7 @@ allowlist_externals = bash tox setenv = - ORCHESTRA_DEPLOYMENT_TYPE = {env:ORCHESTRA_DEPLOYMENT_TYPE:k8s} + ORCHESTRA_DEPLOYMENT_TYPE = {env:ORCHESTRA_DEPLOYMENT_TYPE:remote} NODE_PORT = {env:NODE_PORT:8080} NODE_URL = {env:NODE_URL:http://localhost} EXCLUDE_NOTEBOOKS = {env:EXCLUDE_NOTEBOOKS:not 10-container-images.ipynb} @@ -890,7 +794,8 @@ commands = bash -c 'k3d --version' ; create registry - bash -c 'k3d registry create registry.localhost --port 5800 -v $HOME/.k3d-registry:/var/lib/registry || true' + bash -c 'docker volume create k3d-registry-vol || true' + bash -c 'k3d registry create registry.localhost --port 5800 -v k3d-registry-vol:/var/lib/registry --no-help || true' ; add patches to host bash -c 'if ! grep -q k3d-registry.localhost /etc/hosts; then sudo {envpython} scripts/patch_hosts.py --add-k3d-registry --fix-docker-hosts; fi' @@ -1018,7 +923,7 @@ commands = tox -e dev.k8s.{posargs:deploy} [testenv:dev.k8s.launch.enclave] -description = Launch a single Enclave on K8s +description = Launch a single Enclave on K8s passenv = HOME, USER setenv= CLUSTER_NAME = {env:CLUSTER_NAME:test-enclave-1} @@ -1040,9 +945,6 @@ allowlist_externals = tox bash commands = - ; purge deployment and dangling resources - tox -e dev.k8s.cleanup - ; destroy cluster bash -c '\ rm -rf .devspace; echo ""; \ @@ -1062,7 +964,7 @@ commands = ; destroy registry bash -c 'k3d registry delete registry.localhost || true' - bash -c 'sudo rm -rf ~/.k3d-registry' + bash -c 'docker volume rm k3d-registry-vol --force || true' [testenv:backend.test.basecpu] description = Base CPU Docker Image Test @@ -1123,7 +1025,7 @@ allowlist_externals = pytest passenv = EXTERNAL_REGISTRY,EXTERNAL_REGISTRY_USERNAME,EXTERNAL_REGISTRY_PASSWORD setenv = - ORCHESTRA_DEPLOYMENT_TYPE = {env:ORCHESTRA_DEPLOYMENT_TYPE:k8s} + ORCHESTRA_DEPLOYMENT_TYPE = {env:ORCHESTRA_DEPLOYMENT_TYPE:remote} NODE_PORT = {env:NODE_PORT:8080} NODE_URL = {env:NODE_URL:http://localhost} EXCLUDE_NOTEBOOKS = {env:EXCLUDE_NOTEBOOKS:}