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

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

Powered by Google App Engine
This is Rietveld 408576698