Skip to content

Commit f89a16d

Browse files
authored
Add/example google cloud storage (#638)
* add example to upload to gcp storage Signed-off-by: vsoch <[email protected]>
1 parent 4c1cd80 commit f89a16d

File tree

4 files changed

+369
-0
lines changed

4 files changed

+369
-0
lines changed

example/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ containers you have in some root from [Galaxy Project Depot](https://depot.galax
99
on your local filesytem to install to an shpc registry (as modules). The script
1010
tries to be efficient and instantiate one remote registry that has all the containers.
1111
See the script header for usage examples.
12+
13+
## Google Cloud Storage
14+
15+
This example walks through installing containers to Google Cloud Storage,
16+
which we do by way of running a virtual machine to pull and then save
17+
the containers there. It would be up to you to then mount the storage in
18+
some respect to interact with your containers.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Google Cloud Storage
2+
3+
This example will walk you through creating a module cache (the module files and containers)
4+
in Google Cloud Storage. We will create a virtual machine to build our modules, and save
5+
to cloud storage. You should have a Google Cloud Project active before you begin.
6+
I usually do:
7+
8+
```bash
9+
$ gcloud auth login
10+
$ gcloud config set project dinodev
11+
```
12+
13+
## Prepare Bucket
14+
15+
Make sure you've [prepared a Google Storage](https://cloud.google.com/appengine/docs/legacy/standard/python/googlecloudstorageclient/setting-up-cloud-storage) bucket.
16+
17+
## Virtual Machine
18+
19+
Prepare a virtual machine with a good amount of RAM and give access to all cloud APIS.
20+
If you intend to scale this and have large containers to pull, it's recommended to create the VM with a large disk
21+
that you can then set to be the `--root`, and also export the `SINGULARITY_CACHE` to be located there.
22+
23+
Make sure Singularity and shpc are installed, and libraries needed to interact
24+
25+
```bash
26+
export DEBIAN_FRONTEND=noninteractive
27+
sudo apt-get update && sudo apt-get update install -y \
28+
python3-pip \
29+
wget \
30+
build-essential \
31+
libseccomp-dev \
32+
libglib2.0-dev \
33+
pkg-config \
34+
squashfs-tools \
35+
cryptsetup \
36+
runc
37+
38+
# install Go
39+
export VERSION=1.20.2 OS=linux ARCH=amd64 && \
40+
wget https://dl.google.com/go/go$VERSION.$OS-$ARCH.tar.gz && \
41+
sudo tar -C /usr/local -xzvf go$VERSION.$OS-$ARCH.tar.gz && \
42+
rm go$VERSION.$OS-$ARCH.tar.gz
43+
44+
echo 'export PATH=/usr/local/go/bin:$PATH' >> ~/.bashrc && \
45+
source ~/.bashrc
46+
47+
# Install Singularity
48+
export VERSION=3.11.0 && \
49+
wget https://github.com/sylabs/singularity/releases/download/v${VERSION}/singularity-ce-${VERSION}.tar.gz && \
50+
tar -xzf singularity-ce-${VERSION}.tar.gz && \
51+
cd singularity-ce-${VERSION}
52+
53+
./mconfig && \
54+
make -C builddir && \
55+
sudo make -C builddir install
56+
57+
# Install Python Modules
58+
pip install singularity-hpc
59+
pip install google-cloud-storage
60+
```
61+
62+
Ensure your local home is on the path (for shpc)
63+
64+
```bash
65+
$ export PATH=~/.local/bin:$PATH
66+
```
67+
68+
Then prepare a text file with the containers you want to build and install.
69+
To generate the entire registry you can do:
70+
71+
```bash
72+
$ shpc show > containers.txt
73+
```
74+
75+
Or create it manually, here is an example (much smaller) set:
76+
77+
```text
78+
r-base
79+
rabbitmq
80+
redis
81+
ruby
82+
singularityhub/singularity-deploy
83+
solr
84+
spack/ubuntu-bionic
85+
tensorflow/tensorflow
86+
tomcat
87+
uvarc/qiime2
88+
vanessa/salad
89+
vault
90+
```
91+
92+
Next, run the script here, targeting your containers.txt. You can use [gcloud compute copy-file](https://cloud.google.com/sdk/gcloud/reference/compute/copy-files)
93+
to get the file to your VM. E.g., this would copy to your home:
94+
95+
```bash
96+
$ gcloud compute copy-files ./google-cloud-storage.py shpc-registry-builder:~/ --zone=us-central1-a
97+
```
98+
99+
The bucket should include the bucket name and prefix that you want to use. **Important** if you intend to scale this
100+
and have large containers to pull, it's recommended to create the VM with a large disk that you can then set to be the `--root`,
101+
and also export the `SINGULARITY_CACHE` to be located there. By default, the install `--root` will be to `~/shpc-cache`.
102+
103+
```bash
104+
$ python3 google-cloud-storage.py containers.txt --bucket gs://flux-operator-storage/shpc-registry
105+
```
106+
107+
Note that you have good control over a bunch of parameters, including whether to keep containers on the file system,
108+
and how many container pulls to do at once (be careful about filling up the filesystem). It's currently set to a small
109+
value (2) to be conservative as I was testing on a small instance.
110+
111+
```bash
112+
$ python3 google-cloud-storage.py --help
113+
```
114+
```console
115+
...
116+
SHPC Google Cloud Storage Adder
117+
118+
positional arguments:
119+
containers Path to containers.txt file
120+
121+
optional arguments:
122+
-h, --help show this help message and exit
123+
--force Force reinstall of module
124+
--bucket BUCKET Bucket name and prefix
125+
--root ROOT Install root for modules and containers
126+
--keep Don't clean up as we go (only recommended if you have a large filesystem)
127+
--workers WORKERS Number of workers to use
128+
--group-size GROUP_SIZE
129+
Number containers to pull at once (before clearing cache)
130+
```
131+
132+
When you are done, your storage will be populated with modules, container images,
133+
134+
![img/storage.png](img/storage.png)
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/env python3
2+
3+
# Run this script on a VM (or a machine with your Google Cloud credentials)
4+
# to upload to storage. You should target a containers.txt file, a single
5+
# line listing of the containers you want to build / install. A container
6+
# that already is at the path will not be replaced.
7+
8+
import argparse
9+
import multiprocessing
10+
import os
11+
import shutil
12+
import sys
13+
import time
14+
15+
from google.cloud import storage
16+
17+
import shpc.utils
18+
from shpc.main import get_client
19+
20+
# Global shpc and storage clients
21+
cli = get_client()
22+
storage_client = storage.Client()
23+
24+
25+
def get_parser():
26+
parser = argparse.ArgumentParser(
27+
description="SHPC Google Cloud Storage Adder",
28+
formatter_class=argparse.RawTextHelpFormatter,
29+
)
30+
parser.add_argument(
31+
"--force", help="Force reinstall of module", default=False, action="store_true"
32+
)
33+
parser.add_argument("containers", help="Path to containers.txt file")
34+
parser.add_argument("--bucket", help="Bucket name and prefix", required=True)
35+
parser.add_argument(
36+
"--root",
37+
help="Install root for modules and containers",
38+
default=os.path.expanduser("~/shpc-cache"),
39+
)
40+
parser.add_argument(
41+
"--keep",
42+
help="Don't clean up as we go (only recommended if you have a large filesystem)",
43+
default=False,
44+
action="store_true",
45+
)
46+
parser.add_argument(
47+
"--workers",
48+
help="Number of workers to use",
49+
default=os.cpu_count(),
50+
type=int,
51+
)
52+
parser.add_argument(
53+
"--group-size",
54+
help="Number containers to pull at once (before clearing cache)",
55+
default=1,
56+
type=int,
57+
)
58+
return parser
59+
60+
61+
def recursive_list(base):
62+
"""
63+
Get all files in a root
64+
"""
65+
for root, _, filenames in os.walk(base):
66+
for filename in filenames:
67+
yield os.path.join(root, filename)
68+
69+
70+
class Task:
71+
"""
72+
Task to pull a Singularity container
73+
"""
74+
75+
def __init__(self, module, bucket, keep=False, force=False):
76+
self.module = module
77+
self.keep = keep
78+
self.bucket = bucket
79+
self.force = force
80+
81+
def run(self):
82+
"""
83+
Run the repo task, uploading the data and taking a pause if needed.
84+
"""
85+
global cli
86+
87+
# First check that it doesn't already exist
88+
bucket_name = self.bucket.replace("gs://", "")
89+
bucket_name, bucket_subdir = bucket_name.split("/", 1)
90+
bucket = storage_client.bucket(bucket_name)
91+
92+
# This will have the name and latest
93+
module = cli.get_module(self.module)
94+
blob_path = os.path.join(
95+
bucket_subdir, self.module, module.tag.name, "module.lua"
96+
)
97+
blob = bucket.blob(blob_path)
98+
99+
# Cut out early if the module exists and we don't force re-install
100+
if blob.exists() and not self.force:
101+
return f"Module {self.module} already exists and force is false, not re-installing."
102+
103+
container_path = cli.install(self.module)
104+
105+
# Upload to Google Cloud Storage
106+
container_base = cli.settings.get("container_base")
107+
relpath = os.path.relpath(container_path, container_base)
108+
module, version, _ = relpath.split(os.sep, 2)
109+
110+
# The module files and container are here
111+
root = os.path.join(container_base, module, version)
112+
for path in recursive_list(root):
113+
# Get path relative to storage
114+
relpath = os.path.relpath(path, container_base)
115+
116+
# Assemble relative path
117+
blob = bucket.blob(os.path.join(bucket_subdir, relpath))
118+
if not blob.exists():
119+
generation_match_precondition = 0
120+
blob.upload_from_filename(
121+
path, if_generation_match=generation_match_precondition
122+
)
123+
124+
# Clean up the root if we don't intend to keep
125+
if not self.keep:
126+
shutil.rmtree(root)
127+
128+
129+
def chunks(listing, size):
130+
"""
131+
Yield sized chunks of a list
132+
"""
133+
assert type(listing) is list, "L is not a list"
134+
for i in range(0, len(listing), size):
135+
yield listing[i : i + size]
136+
137+
138+
class TaskRunner:
139+
"""
140+
A task runner knows how run tasks!
141+
"""
142+
143+
def __init__(self, workers=4):
144+
self.workers = workers
145+
self.tasks = []
146+
147+
def add_task(self, task):
148+
self.tasks.append(task)
149+
150+
def run(self):
151+
"""
152+
Run the tasks!
153+
"""
154+
items = []
155+
with multiprocessing.Pool(processes=self.workers) as pool:
156+
for result in pool.map(run_task, self.tasks):
157+
# This is a smaller list of packages/repo metadata pushes
158+
print(f"Installed {result}")
159+
items.append(result)
160+
161+
# Return all results from running the task
162+
return items
163+
164+
165+
def run_task(t):
166+
"""
167+
Anything with a run function can be provided as a task.
168+
"""
169+
return t.run()
170+
171+
172+
def main():
173+
parser = get_parser()
174+
175+
# If an error occurs while parsing the arguments, the interpreter will exit with value 2
176+
args, extra = parser.parse_known_args()
177+
178+
if not args.bucket:
179+
sys.exit("A --bucket is required, name and desired path prefix.")
180+
181+
# Show args to the user
182+
print(" containers: %s" % args.containers)
183+
print(" bucket: %s" % args.bucket)
184+
print(" workers: %s" % args.workers)
185+
print(" root: %s" % args.root)
186+
print(" force install: %s" % args.force)
187+
print("keep containers: %s" % args.keep)
188+
time.sleep(3)
189+
190+
if not os.path.exists(args.containers):
191+
sys.exit(f"Path {args.containers} does not exist.")
192+
193+
global cli
194+
195+
# Create the root to install to
196+
print(f"Containers and modules will install to {args.root}")
197+
if not os.path.exists(args.root):
198+
os.makedirs(args.root)
199+
200+
cli.settings.set("module_base", args.root)
201+
cli.settings.set("container_base", args.root)
202+
203+
# Ensure we start with the populated modules
204+
cli.registry.iter_modules()
205+
206+
# We break into groups of 5 so we can clear the cache in-between runs
207+
containers = shpc.utils.read_file(args.containers).split("\n")
208+
209+
# For each container, install
210+
for group in chunks(containers, args.group_size):
211+
# When we start a group, clean up cache from last
212+
os.system("singularity cache clean --force")
213+
214+
# Create a task runner to do the installs
215+
runner = TaskRunner(args.workers)
216+
for container in group:
217+
container = container.strip()
218+
if not container:
219+
continue
220+
task = Task(container, args.bucket, args.keep, args.force)
221+
runner.add_task(task)
222+
223+
# Run all tasks to install containers
224+
runner.run()
225+
226+
227+
if __name__ == "__main__":
228+
main()
46.2 KB
Loading

0 commit comments

Comments
 (0)