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

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