Skip to content

Commit 0dfa9f2

Browse files
authored
Merge pull request #54 from vsoch/add/client
Add/client
2 parents 3de5e8e + 30fd3aa commit 0dfa9f2

File tree

8 files changed

+456
-35
lines changed

8 files changed

+456
-35
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Compare Singularity Hub containers
2+
3+
# This is a simple script to use the singularity command line tool to download containers
4+
# (using Singularity, Section 1) and compare build specs (using Singularity Hub API, Section 2) and to
5+
# compare the containers themselves using singularity python (Section 3)
6+
7+
container_names = ['vsoch/singularity-hello-world',
8+
'researchapps/quantum_state_diffusion',
9+
'vsoch/pefinder']
10+
11+
from singularity.hub.client import Client
12+
from singularity.package import get_image_hash
13+
14+
import tempfile
15+
import os
16+
import demjson
17+
import pandas
18+
import shutil
19+
20+
shub = Client() # Singularity Hub Client
21+
results = dict()
22+
23+
# Let's keep images in a temporary folder
24+
storage = tempfile.mkdtemp()
25+
os.chdir(storage)
26+
27+
# We will keep a table of information
28+
columns = ['name','build_time_seconds','hash','size','commit','estimated_os']
29+
df = pandas.DataFrame(columns=columns)
30+
31+
for container_name in container_names:
32+
33+
# Retrieve the container based on the name
34+
collection = shub.get_collection(container_name)
35+
container_ids = collection['container_set']
36+
containers = []
37+
for container_id in container_ids:
38+
manifest = shub.get_container(container_id)
39+
containers.append(manifest)
40+
image = shub.pull_container(manifest,
41+
download_folder=storage,
42+
name="%s.img.gz" %(manifest['version']))
43+
# Get hash of file
44+
hashes.append(get_image_hash(image))
45+
df.loc['%s-%s' %(container_name,manifest['version'])]
46+
47+
results[container_name] = {'collection':collection,
48+
'containers':containers}
49+
50+
shutil.rmtree(storage)

singularity/cli.py

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,17 @@ def execute(self,image_path,command,writable=False,contain=False):
126126
if writable == True:
127127
sudo = True
128128

129-
cmd = cmd + [image_path,command]
129+
if not isinstance(list,command):
130+
command = command.split(' ')
131+
132+
cmd = cmd + [image_path] + command
130133

131134
# Run the command
132135
return self.run_command(cmd,sudo=sudo)
133136

134137

135138

136-
def export(self,image_path,pipe=False,output_file=None,command=None,export_format="tar"):
139+
def export(self,image_path,pipe=False,output_file=None,export_format="tar"):
137140
'''export will export an image, sudo must be used.
138141
:param image_path: full path to image
139142
:param pipe: export to pipe and not file (default, False)
@@ -184,8 +187,10 @@ def importcmd(self,image_path,input_source,import_type=None,command=None):
184187
sudo = True
185188
if import_type == "tar":
186189
cmd = ['singularity','import','--file',input_source]
187-
if command != None:
188-
cmd = cmd + ["--command",command]
190+
if command is not None:
191+
if not isinstance(list,command):
192+
command = command.split(' ')
193+
cmd = cmd + ["--command"] + command
189194
cmd.append(image_path)
190195
return self.run_command(cmd,sudo=sudo)
191196
else:
@@ -199,7 +204,7 @@ def pull(self,image_path):
199204
'''pull will pull a singularity hub image
200205
:param image_path: full path to image
201206
'''
202-
if not image_path.startswit('shub://'):
207+
if not image_path.startswith('shub://'):
203208
bot.logger.error("pull is only valid for the shub://uri, %s is invalid.",image_name)
204209
sys.exit(1)
205210

@@ -211,44 +216,57 @@ def pull(self,image_path):
211216

212217

213218

214-
def run(self,image_path,command,writable=False,contain=False):
215-
'''run will run a command inside the container, probably not intended for within python
219+
def run(self,image_path,args=None,writable=False,contain=False):
220+
'''run will run the container, with or withour arguments (which
221+
should be provided in a list)
216222
:param image_path: full path to singularity image
217-
:param command: command to send to container
223+
:param args: args to include with the run
218224
'''
219225
sudo = False
220226
cmd = ["singularity","run"]
221227
cmd = self.add_flags(cmd,writable=writable,contain=contain)
228+
cmd = cmd + [image_path]
222229

223230
# Conditions for needing sudo
224231
if writable == True:
225232
sudo = True
226-
227-
cmd = cmd + [image_path,command]
228-
229-
# Run the command
230-
return self.run_command(cmd,sudo=sudo)
231-
232-
233-
def start(self,image_path,writable=False,contain=False):
234-
'''start will start a container
233+
234+
if args is not None:
235+
if not isinstance(list,args):
236+
args = command.split(' ')
237+
cmd = cmd + args
238+
239+
result = self.run_command(cmd,sudo=sudo)
240+
if isinstance(result,bytes):
241+
result = result.decode('utf-8')
242+
result = result.strip('\n')
243+
try:
244+
result = json.loads(result)
245+
except:
246+
pass
247+
return result
248+
249+
250+
def get_labels(self,image_path):
251+
'''get_labels will return all labels defined in the image
235252
'''
236-
sudo = False
237-
cmd = ['singularity','start']
238-
cmd = self.add_flags(cmd,writable=writable,contain=contain)
239-
if writable == True:
240-
sudo = True
241-
242-
cmd.append(image_path)
243-
return self.run_command(cmd,sudo=sudo)
244-
245-
246-
def stop(self,image_path):
247-
'''stop will stop a container
253+
cmd = ['singularity','exec',image_path,'cat','/.singularity/labels.json']
254+
labels = self.run_command(cmd)
255+
return json.loads(labels.decode('utf-8'))
256+
257+
258+
def get_args(self,image_path):
259+
'''get_args will return the subset of labels intended to be arguments
260+
(in format SINGULARITY_RUNSCRIPT_ARG_*
248261
'''
249-
cmd = ['singularity','stop',image_path]
250-
return self.run_command(cmd)
251-
262+
labels = self.get_labels(image_path)
263+
args = dict()
264+
for label,values in labels.items():
265+
if re.search("^SINGULARITY_RUNSCRIPT_ARG",label):
266+
vartype = label.split('_')[-1].lower()
267+
if vartype in ["str","float","int","bool"]:
268+
args[vartype] = values.split(',')
269+
return args
252270

253271

254272
def add_flags(self,cmd,writable,contain):
@@ -267,9 +285,11 @@ def add_flags(self,cmd,writable,contain):
267285
return cmd
268286

269287

270-
######################################################################################################
288+
289+
290+
#################################################################################
271291
# HELPER FUNCTIONS
272-
######################################################################################################
292+
#################################################################################
273293

274294
def get_image(image,return_existed=False,sudopw=None,size=None,debug=False):
275295
'''get_image will return the file, if it exists, or if it's docker or

singularity/hub/__init__.py

Whitespace-only changes.

singularity/hub/auth.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env python
2+
3+
'''
4+
auth.py: authentication functions for singularity hub api
5+
currently no token / auth for private collections
6+
'''
7+
8+
from singularity.logman import bot
9+
import os
10+
import sys
11+
12+
13+
def get_headers(token=None):
14+
'''get_headers will return a simple default header for a json
15+
post. This function will be adopted as needed.
16+
:param token: an optional token to add for auth
17+
'''
18+
headers = dict()
19+
headers["Content-Type"] = "application/json"
20+
if token!=None:
21+
headers["Authorization"] = "Bearer %s" %(token)
22+
23+
header_names = ",".join(list(headers.keys()))
24+
bot.logger.debug("Headers found: %s",header_names)
25+
return headers

singularity/hub/base.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python
2+
3+
'''
4+
base.py: base module for working with singularity hub api. Right
5+
now serves to hold defaults.
6+
7+
'''
8+
9+
from singularity.hub.utils import (
10+
parse_container_name,
11+
is_number,
12+
api_get,
13+
api_post
14+
)
15+
16+
import os
17+
import sys
18+
from singularity.logman import bot
19+
20+
api_base = "https://singularity-hub.org/api"
21+
22+
def get_template(container_name,get_type=None):
23+
'''get a container/collection or return None.
24+
'''
25+
if get_type == None:
26+
get_type = "container"
27+
get_type = get_type.lower().replace(' ','')
28+
29+
result = None
30+
image = parse_container_name(container_name)
31+
if is_number(image):
32+
url = "%s/%ss/%s" %(api_base,get_type,image)
33+
34+
elif image['user'] is not None and image['repo_name'] is not None:
35+
url = "%s/%s/%s/%s" %(api_base,
36+
get_type,
37+
image['user'],
38+
image['repo_name'])
39+
40+
if image['repo_tag'] is not None and get_type is not "collection":
41+
url = "%s:%s" %(url,image['repo_tag'])
42+
43+
result = api_get(url)
44+
45+
return result
46+
47+
48+
def download_image(manifest,download_folder=None,extract=True,name=None):
49+
'''download_image will download a singularity image from singularity
50+
hub to a download_folder, named based on the image version (commit id)
51+
:param manifest: the manifest obtained with get_manifest
52+
:param download_folder: the folder to download to, if None, will be pwd
53+
:param extract: if True, will extract image to .img and return that.
54+
:param name: if defined, use custom set image name instead of default
55+
'''
56+
if name is not None:
57+
image_file = name
58+
else:
59+
image_file = get_image_name(manifest)
60+
61+
print("Found image %s:%s" %(manifest['name'],manifest['branch']))
62+
print("Downloading image... %s" %(image_file))
63+
64+
if download_folder != None:
65+
image_file = "%s/%s" %(download_folder,image_file)
66+
url = manifest['image']
67+
image_file = api_get(url,stream_to=image_file)
68+
if extract == True:
69+
print("Decompressing %s" %image_file)
70+
os.system('gzip -d -f %s' %(image_file))
71+
image_file = image_file.replace('.gz','')
72+
return image_file
73+
74+
75+
# Various Helpers ---------------------------------------------------------------------------------
76+
def get_image_name(manifest,extension='img.gz',use_hash=False):
77+
'''get_image_name will return the image name for a manifest
78+
:param manifest: the image manifest with 'image' as key with download link
79+
:param use_hash: use the image hash instead of name
80+
'''
81+
if not use_hash:
82+
image_name = "%s-%s.%s" %(manifest['name'].replace('/','-'),
83+
manifest['branch'].replace('/','-'),
84+
extension)
85+
else:
86+
image_url = os.path.basename(unquote(manifest['image']))
87+
image_name = re.findall(".+[.]%s" %(extension),image_url)
88+
if len(image_name) > 0:
89+
image_name = image_name[0]
90+
else:
91+
bot.logger.error("Singularity Hub Image not found with expected extension %s, exiting.",extension)
92+
sys.exit(1)
93+
94+
bot.logger.info("Singularity Hub Image: %s", image_name)
95+
return image_name

singularity/hub/client.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python
2+
3+
'''
4+
client.py: simple client for singularity hub api
5+
6+
'''
7+
8+
from singularity.hub.auth import (
9+
get_headers
10+
)
11+
12+
from singularity.logman import bot
13+
from singularity.hub.base import (
14+
get_template,
15+
download_image
16+
)
17+
18+
import demjson
19+
20+
class Client(object):
21+
22+
23+
def __init__(self, token=None):
24+
25+
if token is not None:
26+
self.token = token
27+
# currently not used
28+
self.headers = get_headers(token=token)
29+
30+
31+
def update_headers(self, headers):
32+
'''update_headers will add headers to the client
33+
:param headers: should be a dictionary of key,value to update/add to header
34+
'''
35+
for key,value in headers.items():
36+
self.client.headers[key] = item
37+
38+
def load_metrics(self,manifest):
39+
'''load metrics about a container build from the manifest
40+
'''
41+
return demjson.decode(manifest['metrics'])
42+
43+
44+
def get_container(self,container_name):
45+
'''get a container or return None.
46+
'''
47+
return get_template(container_name,"container")
48+
49+
50+
def pull_container(self,manifest,download_folder=None,extract=True,name=None):
51+
'''pull a container to the local machine'''
52+
return download_image(manifest=manifest,
53+
download_folder=download_folder,
54+
extract=extract,
55+
name=name)
56+
57+
58+
59+
def get_collection(self,container_name):
60+
'''get a container collection or return None.
61+
'''
62+
return get_template(container_name,"collection")

0 commit comments

Comments
 (0)