Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1200)

Side by Side Diff: infra/bots/assets/asset_utils.py

Issue 2085473002: Add CIPD support for bot assets (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Path fixes Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « .gitignore ('k') | infra/bots/assets/asset_utils_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # 2 #
3 # Copyright 2016 Google Inc. 3 # Copyright 2016 Google Inc.
4 # 4 #
5 # Use of this source code is governed by a BSD-style license that can be 5 # Use of this source code is governed by a BSD-style license that can be
6 # found in the LICENSE file. 6 # found in the LICENSE file.
7 7
8 8
9 """Utilities for managing assets.""" 9 """Utilities for managing assets."""
10 10
11 11
12 import argparse 12 import argparse
13 import json
13 import os 14 import os
14 import shlex 15 import shlex
15 import shutil 16 import shutil
16 import subprocess 17 import subprocess
17 import sys 18 import sys
18 19
19 SKIA_DIR = os.path.abspath(os.path.realpath(os.path.join( 20 INFRA_BOTS_DIR = os.path.abspath(os.path.realpath(os.path.join(
20 os.path.dirname(os.path.abspath(__file__)), 21 os.path.dirname(os.path.abspath(__file__)), os.pardir)))
21 os.pardir, os.pardir, os.pardir)))
22 INFRA_BOTS_DIR = os.path.join(SKIA_DIR, 'infra', 'bots')
23 sys.path.insert(0, INFRA_BOTS_DIR) 22 sys.path.insert(0, INFRA_BOTS_DIR)
24 import utils 23 import utils
25 import zip_utils 24 import zip_utils
26 25
27 26
28 ASSETS_DIR = os.path.join(INFRA_BOTS_DIR, 'assets') 27 ASSETS_DIR = os.path.join(INFRA_BOTS_DIR, 'assets')
28 CIPD_PACKAGE_NAME_TMPL = 'skia/bots/%s'
29 DEFAULT_CIPD_SERVICE_URL = 'https://chrome-infra-packages.appspot.com'
29 DEFAULT_GS_BUCKET = 'skia-buildbots' 30 DEFAULT_GS_BUCKET = 'skia-buildbots'
30 GS_SUBDIR_TMPL = 'gs://%s/assets/%s' 31 GS_SUBDIR_TMPL = 'gs://%s/assets/%s'
31 GS_PATH_TMPL = '%s/%s.zip' 32 GS_PATH_TMPL = '%s/%s.zip'
33 SKIA_DIR = os.path.abspath(os.path.join(INFRA_BOTS_DIR, os.pardir, os.pardir))
34 TAG_PROJECT_SKIA = 'project:skia'
35 TAG_VERSION_PREFIX = 'version:'
36 TAG_VERSION_TMPL = '%s%%s' % TAG_VERSION_PREFIX
rmistry 2016/06/22 12:41:40 Nit: Could you group some of these constants to ma
borenet 2016/06/28 11:23:19 Done.
32 VERSION_FILENAME = 'VERSION' 37 VERSION_FILENAME = 'VERSION'
33 ZIP_BLACKLIST = ['.git', '.svn', '*.pyc', '.DS_STORE'] 38 ZIP_BLACKLIST = ['.git', '.svn', '*.pyc', '.DS_STORE']
34 39
35 40
36 class _GSWrapper(object): 41 class CIPDStore(object):
42 """Wrapper object for CIPD."""
43 def __init__(self, cipd_url=DEFAULT_CIPD_SERVICE_URL):
44 cipd = 'cipd'
45 platform = 'linux64'
46 if sys.platform == 'darwin':
47 platform = 'mac64'
48 elif sys.platform == 'win32':
49 platform = 'win64'
50 cipd = 'cipd.exe'
51 self._cipd_path = os.path.join(INFRA_BOTS_DIR, 'tools', 'luci-go', platform)
52 self._cipd = os.path.join(self._cipd_path, cipd)
53 self._cipd_url = cipd_url
54 self._check_setup()
55
56 def _check_setup(self):
57 """Verify that we have the CIPD binary and that we're authenticated."""
58 try:
59 subprocess.check_call([self._cipd, 'auth-info'])
60 except OSError:
61 cipd_sha1_path = os.path.join(self._cipd_path, 'cipd.sha1')
62 raise Exception('CIPD binary not found in %s. You may need to run:\n\n'
63 '$ download_from_google_storage -s %s'
64 ' --bucket chromium-luci' % (self._cipd, cipd_sha1_path))
65 except subprocess.CalledProcessError:
66 raise Exception('CIPD not authenticated. You may need to run:\n\n'
67 '$ %s auth-login' % self._cipd)
68
69 def _run(self, cmd):
70 """Run the given command."""
71 subprocess.check_call(
72 [self._cipd]
73 + cmd
74 + ['--service-url', self._cipd_url]
75 )
76
77 def _json_output(self, cmd):
78 """Run the given command, return the JSON output."""
79 with utils.tmp_dir():
80 json_output = os.path.join(os.getcwd(), 'output.json')
81 self._run(cmd + ['--json-output', json_output])
82 with open(json_output) as f:
83 parsed = json.load(f)
84 return parsed.get('result', [])
85
86 def _search(self, pkg_name):
87 res = self._json_output(['search', pkg_name, '--tag', TAG_PROJECT_SKIA])
88 return [r['instance_id'] for r in res]
89
90 def _describe(self, pkg_name, instance_id):
91 """Obtain details about the given package and instance ID."""
92 return self._json_output(['describe', pkg_name, '--version', instance_id])
93
94 def get_available_versions(self, name):
95 """List available versions of the asset."""
96 pkg_name = CIPD_PACKAGE_NAME_TMPL % name
97 versions = []
98 for instance_id in self._search(pkg_name):
99 details = self._describe(pkg_name, instance_id)
100 for tag in details.get('tags'):
101 tag_name = tag.get('tag', '')
102 if tag_name.startswith(TAG_VERSION_PREFIX):
103 trimmed = tag_name[len(TAG_VERSION_PREFIX):]
104 try:
105 versions.append(int(trimmed))
106 except ValueError:
107 pass
rmistry 2016/06/22 12:41:40 Raise an exception here? seems like this should no
borenet 2016/06/28 11:23:19 Done.
108 versions.sort()
109 return versions
110
111 def upload(self, name, version, target_dir):
112 """Create a CIPD package."""
113 self._run([
114 'create',
115 '--name', CIPD_PACKAGE_NAME_TMPL % name,
116 '--in', target_dir,
117 '--tag', TAG_PROJECT_SKIA,
118 '--tag', TAG_VERSION_TMPL % version,
119 ])
120
121 def download(self, name, version, target_dir):
122 """Download a CIPD package."""
123 pkg_name = CIPD_PACKAGE_NAME_TMPL % name
124 version_tag = TAG_VERSION_TMPL % version
125 target_dir = os.path.abspath(target_dir)
126 with utils.tmp_dir():
127 infile = os.path.join(os.getcwd(), 'input')
128 with open(infile, 'w') as f:
129 f.write('%s %s' % (pkg_name, version_tag))
130 self._run([
131 'ensure',
132 '--root', target_dir,
133 '--list', infile,
134 ])
135
136 def delete_contents(self, name):
137 """Delete data for the given asset."""
138 self._run(['pkg-delete', CIPD_PACKAGE_NAME_TMPL % name])
139
140
141 class GSStore(object):
37 """Wrapper object for interacting with Google Storage.""" 142 """Wrapper object for interacting with Google Storage."""
38 def __init__(self, gsutil): 143 def __init__(self, gsutil=None, bucket=DEFAULT_GS_BUCKET):
39 gsutil = os.path.abspath(gsutil) if gsutil else 'gsutil' 144 gsutil = os.path.abspath(gsutil) if gsutil else 'gsutil'
40 self._gsutil = [gsutil] 145 self._gsutil = [gsutil]
41 if gsutil.endswith('.py'): 146 if gsutil.endswith('.py'):
42 self._gsutil = ['python', gsutil] 147 self._gsutil = ['python', gsutil]
148 self._gs_bucket = bucket
43 149
44 def copy(self, src, dst): 150 def copy(self, src, dst):
45 """Copy src to dst.""" 151 """Copy src to dst."""
46 subprocess.check_call(self._gsutil + ['cp', src, dst]) 152 subprocess.check_call(self._gsutil + ['cp', src, dst])
47 153
48 def list(self, path): 154 def list(self, path):
49 """List objects in the given path.""" 155 """List objects in the given path."""
50 try: 156 try:
51 return subprocess.check_output(self._gsutil + ['ls', path]).splitlines() 157 return subprocess.check_output(self._gsutil + ['ls', path]).splitlines()
52 except subprocess.CalledProcessError: 158 except subprocess.CalledProcessError:
53 # If the prefix does not exist, we'll get an error, which is okay. 159 # If the prefix does not exist, we'll get an error, which is okay.
54 return [] 160 return []
55 161
162 def get_available_versions(self, name):
163 """Return the existing version numbers for the asset."""
164 files = self.list(GS_SUBDIR_TMPL % (self._gs_bucket, name))
165 bnames = [os.path.basename(f) for f in files]
166 suffix = '.zip'
167 versions = [int(f[:-len(suffix)]) for f in bnames if f.endswith(suffix)]
168 versions.sort()
169 return versions
170
171 def upload(self, name, version, target_dir):
172 """Upload to GS."""
173 target_dir = os.path.abspath(target_dir)
174 with utils.tmp_dir():
175 zip_file = os.path.join(os.getcwd(), '%d.zip' % version)
176 zip_utils.zip(target_dir, zip_file, blacklist=ZIP_BLACKLIST)
177 gs_path = GS_PATH_TMPL % (GS_SUBDIR_TMPL % (self._gs_bucket, name),
178 str(version))
179 self.copy(zip_file, gs_path)
180
181 def download(self, name, version, target_dir):
182 """Download from GS."""
183 gs_path = GS_PATH_TMPL % (GS_SUBDIR_TMPL % (self._gs_bucket, name),
184 str(version))
185 target_dir = os.path.abspath(target_dir)
186 with utils.tmp_dir():
187 zip_file = os.path.join(os.getcwd(), '%d.zip' % version)
188 self.copy(gs_path, zip_file)
189 zip_utils.unzip(zip_file, target_dir)
190
191 def delete_contents(self, name):
192 """Delete data for the given asset."""
193 gs_path = GS_SUBDIR_TMPL % (self._gs_bucket, name)
194 attempt_delete = True
195 try:
196 subprocess.check_call(['gsutil', 'ls', gs_path])
197 except subprocess.CalledProcessError:
198 attempt_delete = False
199 if attempt_delete:
200 subprocess.check_call(['gsutil', 'rm', '-rf', gs_path])
201
202
203 class MultiStore(object):
204 """Wrapper object which uses CIPD as the primary store and GS for backup."""
205 def __init__(self, cipd_url=DEFAULT_CIPD_SERVICE_URL,
206 gsutil=None, gs_bucket=DEFAULT_GS_BUCKET):
207 self._cipd = CIPDStore(cipd_url=cipd_url)
208 self._gs = GSStore(gsutil=gsutil, bucket=gs_bucket)
209
210 def get_available_versions(self, name):
211 return self._cipd.get_available_versions(name)
212
213 def upload(self, name, version, target_dir):
214 self._cipd.upload(name, version, target_dir)
215 self._gs.upload(name, version, target_dir)
216
217 def download(self, name, version, target_dir):
218 self._cipd.download(name, version, target_dir)
219
220 def delete_contents(self, name):
221 self._cipd.delete_contents(name)
222 self._gs.delete_contents(name)
223
56 224
57 def _prompt(prompt): 225 def _prompt(prompt):
rmistry 2016/06/22 12:41:40 Inline? Used in one place and taking up 5 lines.
borenet 2016/06/28 11:23:19 It needs to be in its own function so that the tes
58 """Prompt for input, return result.""" 226 """Prompt for input, return result."""
59 return raw_input(prompt) 227 return raw_input(prompt)
60 228
61 229
62 class Asset(object): 230 class Asset(object):
63 def __init__(self, name, gs_bucket=DEFAULT_GS_BUCKET, gsutil=None): 231 def __init__(self, name, store):
64 self._gs = _GSWrapper(gsutil) 232 self._store = store
65 self._gs_subdir = GS_SUBDIR_TMPL % (gs_bucket, name)
66 self._name = name 233 self._name = name
67 self._dir = os.path.join(ASSETS_DIR, self._name) 234 self._dir = os.path.join(ASSETS_DIR, self._name)
68 235
69 @property 236 @property
70 def version_file(self): 237 def version_file(self):
71 """Return the path to the version file for this asset.""" 238 """Return the path to the version file for this asset."""
72 return os.path.join(self._dir, VERSION_FILENAME) 239 return os.path.join(self._dir, VERSION_FILENAME)
73 240
74 def get_current_version(self): 241 def get_current_version(self):
75 """Obtain the current version of the asset.""" 242 """Obtain the current version of the asset."""
76 if not os.path.isfile(self.version_file): 243 if not os.path.isfile(self.version_file):
77 return -1 244 return -1
78 with open(self.version_file) as f: 245 with open(self.version_file) as f:
79 return int(f.read()) 246 return int(f.read())
80 247
81 def get_available_versions(self): 248 def get_available_versions(self):
82 """Return the existing version numbers for this asset.""" 249 """Return the existing version numbers for this asset."""
83 files = self._gs.list(self._gs_subdir) 250 return self._store.get_available_versions(self._name)
84 bnames = [os.path.basename(f) for f in files]
85 suffix = '.zip'
86 versions = [int(f[:-len(suffix)]) for f in bnames if f.endswith(suffix)]
87 versions.sort()
88 return versions
89 251
90 def get_next_version(self): 252 def get_next_version(self):
91 """Find the next available version number for the asset.""" 253 """Find the next available version number for the asset."""
92 versions = self.get_available_versions() 254 versions = self.get_available_versions()
93 if len(versions) == 0: 255 if len(versions) == 0:
94 return 0 256 return 0
95 return versions[-1] + 1 257 return versions[-1] + 1
96 258
97 def download_version(self, version, target_dir): 259 def download_version(self, version, target_dir):
98 """Download the specified version of the asset.""" 260 """Download the specified version of the asset."""
99 gs_path = GS_PATH_TMPL % (self._gs_subdir, str(version)) 261 self._store.download(self._name, version, target_dir)
100 target_dir = os.path.abspath(target_dir)
101 with utils.tmp_dir():
102 zip_file = os.path.join(os.getcwd(), '%d.zip' % version)
103 self._gs.copy(gs_path, zip_file)
104 zip_utils.unzip(zip_file, target_dir)
105 262
106 def download_current_version(self, target_dir): 263 def download_current_version(self, target_dir):
107 """Download the version of the asset specified in its version file.""" 264 """Download the version of the asset specified in its version file."""
108 v = self.get_current_version() 265 v = self.get_current_version()
109 self.download_version(v, target_dir) 266 self.download_version(v, target_dir)
110 267
111 def upload_new_version(self, target_dir, commit=False): 268 def upload_new_version(self, target_dir, commit=False):
112 """Upload a new version and update the version file for the asset.""" 269 """Upload a new version and update the version file for the asset."""
113 version = self.get_next_version() 270 version = self.get_next_version()
114 target_dir = os.path.abspath(target_dir) 271 self._store.upload(self._name, version, target_dir)
115 with utils.tmp_dir():
116 zip_file = os.path.join(os.getcwd(), '%d.zip' % version)
117 zip_utils.zip(target_dir, zip_file, blacklist=ZIP_BLACKLIST)
118 gs_path = GS_PATH_TMPL % (self._gs_subdir, str(version))
119 self._gs.copy(zip_file, gs_path)
120 272
121 def _write_version(): 273 def _write_version():
122 with open(self.version_file, 'w') as f: 274 with open(self.version_file, 'w') as f:
123 f.write(str(version)) 275 f.write(str(version))
124 subprocess.check_call([utils.GIT, 'add', self.version_file]) 276 subprocess.check_call([utils.GIT, 'add', self.version_file])
125 277
126 with utils.chdir(SKIA_DIR): 278 with utils.chdir(SKIA_DIR):
127 if commit: 279 if commit:
128 with utils.git_branch(): 280 with utils.git_branch():
129 _write_version() 281 _write_version()
130 subprocess.check_call([ 282 subprocess.check_call([
131 utils.GIT, 'commit', '-m', 'Update %s version' % self._name]) 283 utils.GIT, 'commit', '-m', 'Update %s version' % self._name])
132 subprocess.check_call([utils.GIT, 'cl', 'upload', '--bypass-hooks']) 284 subprocess.check_call([utils.GIT, 'cl', 'upload', '--bypass-hooks'])
133 else: 285 else:
134 _write_version() 286 _write_version()
135 287
136 @classmethod 288 @classmethod
rmistry 2016/06/22 12:41:41 Why make this a class method? name and store can b
borenet 2016/06/28 11:23:19 This is a helper function for adding an asset, whe
137 def add(cls, name, gs_bucket=DEFAULT_GS_BUCKET, gsutil=None): 289 def add(cls, name, store):
138 """Add an asset.""" 290 """Add an asset."""
139 asset = cls(name, gs_bucket=gs_bucket, gsutil=gsutil) 291 asset = cls(name, store)
140 if os.path.isdir(asset._dir): 292 if os.path.isdir(asset._dir):
141 raise Exception('Asset %s already exists!' % asset._name) 293 raise Exception('Asset %s already exists!' % asset._name)
142 294
143 print 'Creating asset in %s' % asset._dir 295 print 'Creating asset in %s' % asset._dir
144 os.mkdir(asset._dir) 296 os.mkdir(asset._dir)
145 def copy_script(script): 297 def copy_script(script):
146 src = os.path.join(ASSETS_DIR, 'scripts', script) 298 src = os.path.join(ASSETS_DIR, 'scripts', script)
147 dst = os.path.join(asset._dir, script) 299 dst = os.path.join(asset._dir, script)
148 print 'Creating %s' % dst 300 print 'Creating %s' % dst
149 shutil.copy(src, dst) 301 shutil.copy(src, dst)
150 subprocess.check_call([utils.GIT, 'add', dst]) 302 subprocess.check_call([utils.GIT, 'add', dst])
151 303
152 for script in ('download.py', 'upload.py', 'common.py'): 304 for script in ('download.py', 'upload.py', 'common.py'):
153 copy_script(script) 305 copy_script(script)
154 resp = _prompt('Add script to automate creation of this asset? (y/n) ') 306 resp = _prompt('Add script to automate creation of this asset? (y/n) ')
155 if resp == 'y': 307 if resp == 'y':
156 copy_script('create.py') 308 copy_script('create.py')
157 copy_script('create_and_upload.py') 309 copy_script('create_and_upload.py')
158 print 'You will need to add implementation to the creation script.' 310 print 'You will need to add implementation to the creation script.'
159 print 'Successfully created asset %s.' % asset._name 311 print 'Successfully created asset %s.' % asset._name
160 return asset 312 return asset
161 313
162 def remove(self): 314 def remove(self, remove_in_store=False):
163 """Remove this asset.""" 315 """Remove this asset."""
164 # Ensure that the asset exists. 316 # Ensure that the asset exists.
165 if not os.path.isdir(self._dir): 317 if not os.path.isdir(self._dir):
166 raise Exception('Asset %s does not exist!' % self._name) 318 raise Exception('Asset %s does not exist!' % self._name)
167 319
320 # Cleanup the store.
321 if remove_in_store:
322 self._store.delete_contents(self._name)
323
168 # Remove the asset. 324 # Remove the asset.
169 subprocess.check_call([utils.GIT, 'rm', '-rf', self._dir]) 325 subprocess.check_call([utils.GIT, 'rm', '-rf', self._dir])
170 if os.path.isdir(self._dir): 326 if os.path.isdir(self._dir):
171 shutil.rmtree(self._dir) 327 shutil.rmtree(self._dir)
172
173 # We *could* remove all uploaded versions of the asset in Google Storage but
174 # we choose not to be that destructive.
OLDNEW
« no previous file with comments | « .gitignore ('k') | infra/bots/assets/asset_utils_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698