[2543] | 1 | """Invirt build utilities. |
---|
| 2 | |
---|
| 3 | This module contains utility functions used by both the invirtibuilder |
---|
| 4 | and the remctl submission scripts that insert items into its queue. |
---|
| 5 | """ |
---|
| 6 | |
---|
| 7 | |
---|
| 8 | import os |
---|
[3042] | 9 | import subprocess |
---|
[2543] | 10 | |
---|
| 11 | from debian_bundle import changelog |
---|
| 12 | |
---|
| 13 | import invirt.common as c |
---|
| 14 | from invirt.config import structs as config |
---|
| 15 | |
---|
| 16 | |
---|
| 17 | _QUEUE_DIR = '/var/lib/invirt-dev/queue' |
---|
| 18 | _REPO_DIR = '/srv/git' |
---|
| 19 | _LOG_DIR = '/var/log/invirt/builds' |
---|
[3036] | 20 | _HOOKS_DIR = '/usr/share/invirt-dev/build-hooks' |
---|
[2543] | 21 | |
---|
| 22 | |
---|
| 23 | class InvalidBuild(ValueError): |
---|
| 24 | pass |
---|
| 25 | |
---|
| 26 | |
---|
| 27 | def getRepo(package): |
---|
| 28 | """Return the path to the git repo for a given package.""" |
---|
[2577] | 29 | return os.path.join(_REPO_DIR, 'invirt/packages', '%s.git' % package) |
---|
[2543] | 30 | |
---|
[3035] | 31 | def ensureValidPackage(package): |
---|
[3028] | 32 | """Perform some basic sanity checks that the requested repo is in a |
---|
| 33 | subdirectory of _REPO_DIR/invirt/packages. This prevents weirdness |
---|
| 34 | such as submitting a package like '../prod/...git'. Also ensures that |
---|
| 35 | the repo exists.""" |
---|
| 36 | # TODO: this might be easier just to regex |
---|
| 37 | repo = os.path.abspath(getRepo(package)) |
---|
| 38 | parent_dir = os.path.dirname(repo) |
---|
| 39 | prefix = os.path.join(_REPO_DIR, 'invirt/packages') |
---|
| 40 | if not parent_dir.startswith(prefix): |
---|
| 41 | raise InvalidBuild('Invalid package name %s' % package) |
---|
| 42 | elif not os.path.exists(repo): |
---|
| 43 | raise InvalidBuild('Nonexisting package %s' % package) |
---|
[2543] | 44 | |
---|
[3035] | 45 | def canonicalize_commit(package, commit, shorten=False): |
---|
| 46 | if shorten: |
---|
| 47 | flags = ['--short'] |
---|
| 48 | else: |
---|
| 49 | flags = [] |
---|
| 50 | return c.captureOutput(['git', 'rev-parse'] + flags + [commit], |
---|
| 51 | cwd=getRepo(package)).strip() |
---|
| 52 | |
---|
[2543] | 53 | def pocketToGit(pocket): |
---|
| 54 | """Map a pocket in the configuration to a git branch.""" |
---|
[2593] | 55 | return getattr(getattr(config.build.pockets, pocket), 'git', pocket) |
---|
[2543] | 56 | |
---|
| 57 | |
---|
| 58 | def pocketToApt(pocket): |
---|
| 59 | """Map a pocket in the configuration to an apt repo pocket.""" |
---|
[2593] | 60 | return getattr(getattr(config.build.pockets, pocket), 'apt', pocket) |
---|
[2543] | 61 | |
---|
| 62 | |
---|
| 63 | def getGitFile(package, ref, path): |
---|
| 64 | """Return the contents of a path from a git ref in a package.""" |
---|
| 65 | return c.captureOutput(['git', 'cat-file', 'blob', '%s:%s' % (ref, path)], |
---|
[3028] | 66 | cwd=getRepo(package)) |
---|
[2543] | 67 | |
---|
| 68 | |
---|
| 69 | def getChangelog(package, ref): |
---|
| 70 | """Get a changelog object for a given ref in a given package. |
---|
| 71 | |
---|
| 72 | This returns a debian_bundle.changelog.Changelog object for a |
---|
| 73 | given ref of a given package. |
---|
| 74 | """ |
---|
| 75 | return changelog.Changelog(getGitFile(package, ref, 'debian/changelog')) |
---|
| 76 | |
---|
[3036] | 77 | def runHook(hook, args=[], stdin_str=None): |
---|
| 78 | """Run a named hook.""" |
---|
| 79 | hook = os.path.join(_HOOKS_DIR, hook) |
---|
| 80 | try: |
---|
| 81 | c.captureOutput([hook] + args, stdin_str=stdin_str) |
---|
| 82 | except OSError: |
---|
| 83 | pass |
---|
[2543] | 84 | |
---|
| 85 | def getVersion(package, ref): |
---|
| 86 | """Get the version of a given package at a particular ref.""" |
---|
| 87 | return getChangelog(package, ref).get_version() |
---|
| 88 | |
---|
| 89 | |
---|
| 90 | def validateBuild(pocket, package, commit): |
---|
| 91 | """Given the parameters of a new build, validate that build. |
---|
| 92 | |
---|
| 93 | The checks this function performs vary based on whether or not the |
---|
| 94 | pocket is configured with allow_backtracking. |
---|
| 95 | |
---|
| 96 | A build of a pocket without allow_backtracking set must be a |
---|
| 97 | fast-forward of the previous revision, and the most recent version |
---|
| 98 | in the changelog most be strictly greater than the version |
---|
| 99 | currently in the repository. |
---|
| 100 | |
---|
| 101 | In all cases, this revision of the package can only have the same |
---|
| 102 | version number as any other revision currently in the apt |
---|
| 103 | repository if they have the same commit ID. |
---|
| 104 | |
---|
| 105 | If it's unspecified, it is assumed that pocket do not |
---|
| 106 | allow_backtracking. |
---|
| 107 | |
---|
| 108 | If this build request fails validation, this function will raise a |
---|
| 109 | InvalidBuild exception, with information about why the validation |
---|
| 110 | failed. |
---|
| 111 | |
---|
| 112 | If this build request can be satisfied by copying the package from |
---|
| 113 | another pocket, then this function returns that pocket. Otherwise, |
---|
| 114 | it returns True. |
---|
| 115 | """ |
---|
[3035] | 116 | ensureValidPackage(package) |
---|
[2543] | 117 | package_repo = getRepo(package) |
---|
| 118 | new_version = getVersion(package, commit) |
---|
| 119 | |
---|
[2834] | 120 | ret = True |
---|
| 121 | |
---|
[2593] | 122 | for p in config.build.pockets: |
---|
[2543] | 123 | if p == pocket: |
---|
| 124 | continue |
---|
| 125 | |
---|
| 126 | b = pocketToGit(p) |
---|
[3042] | 127 | try: |
---|
| 128 | current_commit = c.captureOutput(['git', 'rev-parse', b], |
---|
| 129 | cwd=package_repo).strip() |
---|
| 130 | except subprocess.CalledProcessError: |
---|
| 131 | # Guess we haven't created this pocket yet |
---|
| 132 | continue |
---|
| 133 | |
---|
[2543] | 134 | current_version = getVersion(package, b) |
---|
| 135 | |
---|
| 136 | if current_version == new_version: |
---|
| 137 | if current_commit == commit: |
---|
[2834] | 138 | ret = p |
---|
[2543] | 139 | else: |
---|
[3028] | 140 | raise InvalidBuild('Version %s of %s already available is in ' |
---|
[2543] | 141 | 'pocket %s from commit %s' % |
---|
| 142 | (new_version, package, p, current_commit)) |
---|
| 143 | |
---|
[3028] | 144 | if not config.build.pockets[pocket].get('allow_backtracking', False): |
---|
[2543] | 145 | branch = pocketToGit(pocket) |
---|
| 146 | current_version = getVersion(package, branch) |
---|
| 147 | if new_version <= current_version: |
---|
| 148 | raise InvalidBuild('New version %s of %s is not newer than ' |
---|
| 149 | 'version %s currently in pocket %s' % |
---|
| 150 | (new_version, package, current_version, pocket)) |
---|
| 151 | |
---|
| 152 | # Almost by definition, A is a fast-forward of B if B..A is |
---|
| 153 | # empty |
---|
[3041] | 154 | if not c.captureOutput(['git', 'rev-list', '%s..%s' % (commit, branch)], |
---|
| 155 | cwd=package_repo): |
---|
[2543] | 156 | raise InvalidBuild('New commit %s of %s is not a fast-forward of' |
---|
| 157 | 'commit currently in pocket %s' % |
---|
| 158 | (commit, package, pocket)) |
---|
| 159 | |
---|
[2834] | 160 | return ret |
---|