55
66'''
77
8- from singularity .api import api_get , api_put
8+ from singularity .api import api_get , api_put , api_post
99from singularity .boutiques import get_boutiques_json
1010from singularity .package import build_from_spec
1111from singularity .utils import get_installdir , read_file , write_file , download_repo
1212
1313from googleapiclient .discovery import build
14- from googleapiclient .http import MediaFileUpload
14+ from oauth2client .client import GoogleCredentials
15+ from googleapiclient import http
1516
1617from glob import glob
1718import httplib2
3334import uuid
3435import 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
3940logging .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