Skip to content

Commit 6f3909d

Browse files
committed
updating singularity-python to work with google storage
1 parent fe0b257 commit 6f3909d

File tree

2 files changed

+90
-216
lines changed

2 files changed

+90
-216
lines changed

singularity/api.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def get_headers(token=None):
6060
return headers
6161

6262

63-
def api_get(url,headers,token=None,data=None):
63+
def api_get(url,headers=None,token=None,data=None):
6464
'''api_get will use requests to get a particular url
6565
:param url: the url to send file to
6666
:param headers: a dictionary with headers for the request
@@ -96,3 +96,19 @@ def api_put(url,headers=None,token=None,data=None):
9696
data=data)
9797

9898
return response
99+
100+
101+
def api_post(url,headers):
102+
'''api_get will use requests to get a particular url
103+
:param url: the url to send file to
104+
:param headers: a dictionary with headers for the request
105+
:param putdata: additional data to add to the request
106+
'''
107+
if data == None:
108+
response = requests.post(url,
109+
headers=headers)
110+
else:
111+
response = requests.post(url,
112+
headers=headers,
113+
data=data)
114+
return response

singularity/build.py

Lines changed: 73 additions & 215 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
66
'''
77

8-
from singularity.api import api_get, api_put
8+
from singularity.api import api_get, api_put, api_post
99
from singularity.boutiques import get_boutiques_json
1010
from singularity.package import build_from_spec
1111
from singularity.utils import get_installdir, read_file, write_file, download_repo
1212

1313
from googleapiclient.discovery import build
14-
from googleapiclient.http import MediaFileUpload
14+
from oauth2client.client import GoogleCredentials
15+
from googleapiclient import http
1516

1617
from glob import glob
1718
import httplib2
@@ -33,208 +34,66 @@
3334
import uuid
3435
import zipfile
3536

36-
api_base = "http://www.singularity-hub.org/api"
37+
shub_api = "http://www.singularity-hub.org/api"
3738

3839
# Log everything to stdout
3940
logging.basicConfig(stream=sys.stdout,level=logging.DEBUG)
4041

41-
def google_drive_connect(credential):
42+
##########################################################################################
43+
# GOOGLE STORAGE API #####################################################################
44+
##########################################################################################
4245

43-
# If it's a dict, assume json and load into credential
44-
if isinstance(credential,str):
45-
credential = json.loads(credential)
46-
47-
if isinstance(credential,dict):
48-
credential = client.Credentials.new_from_json(json.dumps(credential))
49-
50-
# If the user has a credential object, check if it's good
51-
if credential.invalid is True:
52-
logging.warning('Storage credential not valid, refreshing.')
53-
credential.refresh()
54-
55-
# Authorize with http
56-
http_auth = credential.authorize(httplib2.Http())
57-
drive_service = build('drive', 'v3', http=http_auth)
58-
return drive_service
59-
60-
61-
def create_folder(drive_service,folder_name,parent_folders=None):
62-
'''create_folder will create a folder (folder_name) in optional parent_folder
63-
if no parent_folder is specified, will be placed at base of drive
64-
:param drive_service: drive service created by google_drive_connect
65-
:param folder_name: the name of the folder to create
66-
:param parent_folders: one or more parent folder names, either a string or list
67-
'''
68-
file_metadata = {
69-
'name' : folder_name,
70-
'mimeType' : 'application/vnd.google-apps.folder'
71-
}
72-
# Do we have one or more parent folders?
73-
if parent_folders != None:
74-
if not isinstance(parent_folders,list):
75-
parent_folders = [parent_folders]
76-
file_metadata ['parents'] = parent_folders
77-
folder = drive_service.files().create(body=file_metadata,
78-
fields='id').execute()
79-
return folder
80-
81-
82-
def create_file(drive_service,folder_id,file_path,file_name=None,verbose=True):
83-
'''create_folder will create a folder (folder_name) in optional parent_folder
84-
if no parent_folder is specified, will be placed at base of drive
85-
:param drive_service: drive service created by google_drive_connect
86-
:param folder_id: the id of the folder to upload to
87-
:param file_path: the path of the file to add
88-
:param file_name: the name for the file. If not specified, will use current file name
89-
:param parent_folders: one or more parent folder names, either a string or list
90-
:param verbose: print out the file type assigned to the file_path
91-
92-
:: note: as this currently is, files with different names in the same folder will be treated
93-
as different. For builds this should only happen when the user requests a rebuild on the same
94-
commit, in which case both versions of the files will endure, but the updated version will be
95-
recorded as latest. I think this is good functionality for reproducibility, although it's a bit
96-
redundant.
97-
98-
'''
99-
if file_name == None:
100-
file_name = os.path.basename(file_path)
101-
102-
mimetype = sniff_extension(file_path,verbose=verbose)
103-
104-
file_metadata = {
105-
'name' : file_name,
106-
'parents': [ folder_id ]
107-
}
108-
109-
logging.info('Creating file %s in folder %s with mimetype %s', file_name,
110-
folder_id,
111-
mimetype)
112-
media = MediaFileUpload(file_path,
113-
mimetype=mimetype,
114-
resumable=True)
115-
116-
new_file = drive_service.files().create(body=file_metadata,
117-
media_body=media,
118-
fields='id').execute()
119-
new_file['name'] = file_name
120-
return new_file
121-
122-
123-
def permissions_callback(request_id, response, exception):
124-
if exception:
125-
logging.error(exception)
126-
else:
127-
logging.info("Permission Id: %s",response.get('id'))
128-
129-
130-
def set_reader_permissions(drive_service,file_ids):
131-
'''set_permission will set a permission level (default is reader) for one
132-
or more files. If anything other than "reader" is used for permission,
133-
email must be provided
134-
:param drive_service: the drive service created with google_drive_connect
135-
:param file_ids: one or more file_ids, should be string or list
136-
'''
137-
138-
new_permission = { 'type': "anyone",
139-
'role': "reader",
140-
'withLink': True }
141-
142-
if isinstance(file_ids,list) == False:
143-
file_ids = [file_ids]
144-
145-
batch = drive_service.new_batch_http_request(callback=permissions_callback)
146-
147-
for file_id in file_ids:
148-
batch.add(drive_service.permissions().create(
149-
fileId=file_id['id'],
150-
body=new_permission))
151-
152-
batch.execute()
46+
def get_storage_service():
47+
credentials = GoogleCredentials.get_application_default()
48+
return build('storage', 'v1', credentials=credentials)
49+
50+
def get_bucket(storage_service,bucket_name):
51+
req = storage_service.buckets().get(bucket=bucket_name)
52+
return req.execute()
15353

15454

155-
def get_folder(drive_service,folder_name=None,create=True,parent_folder=None):
55+
def upload_file(storage_service,bucket,bucket_path,file_name,verbose=True):
15656
'''get_folder will return the folder with folder_name, and if create=True,
15757
will create it if not found. If folder is found or created, the metadata is
15858
returned, otherwise None is returned
159-
:param drive_service: the drive_service created from google_drive_connect
160-
:param folder_name: the name of the folder to search for, item ['title'] field
161-
:param parent_folder: a parent folder to retrieve, will look at base if none specified.
162-
'''
163-
# Default folder_name (for base) is singularity-hub
164-
if folder_name == None:
165-
folder_name = 'singularity-hub'
166-
167-
# If we don't specify a parent folder, a different folder with an identical name is created
168-
if parent_folder == None:
169-
folders = drive_service.files().list(q='mimeType="application/vnd.google-apps.folder"').execute()
170-
else:
171-
query = 'mimeType="application/vnd.google-apps.folder" and "%s" in parents' %(parent_folder)
172-
folders = drive_service.files().list(q=query).execute()
173-
174-
# Look for the folder in the results
175-
for folder in folders['files']:
176-
if folder['name'] == folder_name:
177-
logging.info("Found folder %s in storage",folder_name)
178-
return folder
179-
180-
logging.info("Did not find %s in storage.",folder_name)
181-
182-
# If folder is not found, create it, else return None
183-
folder = None
184-
if create == True:
185-
logging.info("Creating folder %s.",folder_name)
186-
folder = create_folder(drive_service,folder_name)
187-
return folder
188-
189-
190-
def get_download_links(build_files):
191-
'''get_files will use a drive_service to return a list of build file objects
192-
:param build_files: a list of build_files, each a dictionary with an id for the file
193-
:returns links: a list of dictionaries with included file links
59+
:param storage_service: the drive_service created from get_storage_service
60+
:param bucket: the bucket object from get_bucket
61+
:param file_name: the name of the file to upload
62+
:param bucket_path: the path to upload to
19463
'''
195-
if not isinstance(build_files,list):
196-
build_files = [build_files]
197-
links = []
198-
for build_file in build_files:
199-
link = "https://drive.google.com/uc?export=download&id=%s" %(build_file['id'])
200-
build_file['link'] = link
201-
links.append(build_file)
202-
return links
203-
204-
205-
def google_drive_setup(drive_service,image_path=None,base_folder=None):
206-
'''google_drive_setup will connect to a Google drive, check for the singularity
207-
folder, and if it doesn't exist, create it, along with other collection and image
208-
metadata. The final upload folder for the image and other stuffs is returned
209-
:param image_path: should be the path to the image, from within the singularity-hub folder
210-
(eg, www.github.com/vsoch/singularity-images). If not defined, a folder with the commit id
211-
will be created in the base of the singularity-hub google drive folder
212-
:param base_folder: the parent (base) folder to write to, default is singularity-hub
213-
'''
214-
if base_folder == None:
215-
base_folder = 'singularity-hub'
216-
singularity_folder = get_folder(drive_service,folder_name=base_folder)
217-
logging.info("Base folder set to %s",base_folder)
218-
219-
# If the user wants a more custom path
220-
if image_path != None:
221-
folders = [x.strip(" ") for x in image_path.split("/")]
222-
logging.info("Storage path set to %s","=>".join(folders))
223-
parent_folder = singularity_folder['id']
224-
225-
# The last folder created, the destination for our files, will be returned
226-
for folder in folders:
227-
singularity_folder = get_folder(drive_service=drive_service,
228-
folder_name=folder,
229-
parent_folder=parent_folder)
230-
parent_folder = singularity_folder['id']
231-
232-
return singularity_folder
233-
234-
235-
def run_build(build_dir=None,spec_file=None,repo_url=None,token=None,size=None,
236-
repo_id=None,commit=None,credential=None,verbose=True,response_url=None,
237-
logfile=None):
64+
# Set up path on bucket
65+
upload_path = "%s/%s" %(bucket['id'],bucket_path)
66+
if upload_path[-1] != '/':
67+
upload_path = "%s/" %(upload_path)
68+
upload_path = "%s%s" %(upload_path,os.path.basename(file_name))
69+
body = {'name': upload_path }
70+
71+
# Create media object with correct mimetype
72+
mimetype = sniff_extension(file_name,verbose=verbose)
73+
media = http.MediaFileUpload(file_name,
74+
mimetype=mimetype,
75+
resumable=True)
76+
request = storage_service.objects().insert(bucket=bucket['id'],
77+
body=body,
78+
predefinedAcl="publicRead",
79+
media_body=media)
80+
return request.execute()
81+
82+
83+
def list_bucket(bucket):
84+
# Create a request to objects.list to retrieve a list of objects.
85+
request = storage_service.objects().list(bucket=bucket['id'],
86+
fields='nextPageToken,items(name,size,contentType)')
87+
# Go through the request and look for the folder
88+
objects = []
89+
while request:
90+
response = request.execute()
91+
objects = objects + response['items']
92+
return objects
93+
94+
95+
def run_build(build_dir=None,spec_file=None,repo_url=None,token=None,size=None,bucket_name=None,
96+
repo_id=None,commit=None,verbose=True,response_url=None,logfile=None):
23897
'''run_build will generate the Singularity build from a spec_file from a repo_url.
23998
If no arguments are required, the metadata api is queried for the values.
24099
:param build_dir: directory to do the build in. If not specified,
@@ -244,7 +103,7 @@ def run_build(build_dir=None,spec_file=None,repo_url=None,token=None,size=None,
244103
:param repo_id: the repo_id to uniquely identify the repo (in case name changes)
245104
:param commit: the commit to checkout. If none provided, will use most recent.
246105
:param size: the size of the image to build. If none set, builds default 1024.
247-
:param credential: the credential to send the image to.
106+
:param bucket_name: the name of the bucket to send files to
248107
:param verbose: print out extra details as we go (default True)
249108
:param token: a token to send back to the server to authenticate adding the build
250109
:param logfile: path to a logfile to read and include path in response to server.
@@ -268,8 +127,8 @@ def run_build(build_dir=None,spec_file=None,repo_url=None,token=None,size=None,
268127
# Get variables from the instance metadata API
269128
metadata = [{'key': 'repo_url', 'value': repo_url, 'return_text': False },
270129
{'key': 'repo_id', 'value': repo_id, 'return_text': True },
271-
{'key': 'credential', 'value': credential, 'return_text': True },
272130
{'key': 'response_url', 'value': response_url, 'return_text': True },
131+
{'key': 'bucket_name', 'value': bucket_name, 'return_text': True },
273132
{'key': 'token', 'value': token, 'return_text': False },
274133
{'key': 'commit', 'value': commit, 'return_text': True },
275134
{'key': 'size', 'value': size, 'return_text': True },
@@ -279,6 +138,9 @@ def run_build(build_dir=None,spec_file=None,repo_url=None,token=None,size=None,
279138
if spec_file == None:
280139
spec_file = "Singularity"
281140

141+
if bucket_name == None:
142+
bucket_name = "singularity-hub"
143+
282144
# Obtain values from build
283145
params = get_build_params(metadata)
284146

@@ -317,35 +179,31 @@ def run_build(build_dir=None,spec_file=None,repo_url=None,token=None,size=None,
317179
image_path = "%s/%s" %(re.sub('^http.+//www[.]','',params['repo_url']),params['commit'])
318180
build_files = glob("%s/*" %(dest_dir))
319181
logging.info("Sending build files %s to storage",'\n'.join(build_files))
320-
drive_service = google_drive_connect(params['credential'])
321-
upload_folder = google_drive_setup(drive_service=drive_service,
322-
image_path=image_path)
323182

324-
# For each file, upload to drive
183+
# Start the storage service, retrieve the bucket
184+
storage_service = get_storage_service()
185+
bucket = get_bucket(storage_service,bucker_name)
186+
187+
# For each file, upload to storage
325188
files = []
326189
for build_file in build_files:
327-
drive_file = create_file(drive_service,
328-
folder_id=upload_folder['id'],
329-
file_path=build_file)
330-
files.append(drive_file)
190+
storage_file = upload_file(storage_service,
191+
bucket=bucket,
192+
bucket_path=image_path,
193+
file_name=build_file)
194+
files.append(storage_file)
331195

332-
# Set readable permissions
333-
set_reader_permissions(drive_service,files)
334-
335-
# Get metadata to return to singularity-hub
336-
download_links = get_download_links(build_files=files)
337196

338197
# If the user has specified a log file, include with data/response
339198
if logfile != None:
340-
log_file = create_file(drive_service,
341-
folder_id=upload_folder['id'],
342-
file_path=logfile)
343-
log_file['name'] = 'log'
199+
log_file = upload_file(storage_service,
200+
bucket=bucket,
201+
bucket_path=image_path,
202+
file_name=logfile)
344203
files.append(log_file)
345-
download_links = download_links + get_download_links(build_files=log_file)
346-
204+
347205
# Finally, package everything to send back to shub
348-
response = {"files": download_links,
206+
response = {"files": files,
349207
"repo_url": params['repo_url'],
350208
"commit": params['commit'],
351209
"repo_id": params['repo_id']}

0 commit comments

Comments
 (0)