qvm-template: Reorder functions.
This commit is contained in:
		
							parent
							
								
									233e411c2f
								
							
						
					
					
						commit
						3d0a39523b
					
				| @ -123,6 +123,31 @@ DlEntry = collections.namedtuple('DlEntry', [ | |||||||
|     'dlsize' |     'dlsize' | ||||||
| ]) | ]) | ||||||
| 
 | 
 | ||||||
|  | def build_version_str(evr): | ||||||
|  |     return '%s:%s-%s' % evr | ||||||
|  | 
 | ||||||
|  | def is_match_spec(name, epoch, version, release, spec): | ||||||
|  |     # Refer to "NEVRA Matching" in the DNF documentation | ||||||
|  |     # NOTE: Currently "arch" is ignored as the templates should be of "noarch" | ||||||
|  |     if epoch != 0: | ||||||
|  |         targets = [ | ||||||
|  |             f'{name}-{epoch}:{version}-{release}', | ||||||
|  |             f'{name}', | ||||||
|  |             f'{name}-{epoch}:{version}' | ||||||
|  |         ] | ||||||
|  |     else: | ||||||
|  |         targets = [ | ||||||
|  |             f'{name}-{epoch}:{version}-{release}', | ||||||
|  |             f'{name}-{version}-{release}', | ||||||
|  |             f'{name}', | ||||||
|  |             f'{name}-{epoch}:{version}', | ||||||
|  |             f'{name}-{version}' | ||||||
|  |         ] | ||||||
|  |     for prio, target in enumerate(targets): | ||||||
|  |         if fnmatch.fnmatch(target, spec): | ||||||
|  |             return True, prio | ||||||
|  |     return False, float('inf') | ||||||
|  | 
 | ||||||
| def query_local(vm): | def query_local(vm): | ||||||
|     return Template( |     return Template( | ||||||
|         vm.features['template-name'], |         vm.features['template-name'], | ||||||
| @ -147,10 +172,135 @@ def is_managed_template(vm): | |||||||
|     return 'template-name' in vm.features \ |     return 'template-name' in vm.features \ | ||||||
|         and vm.name == vm.features['template-name'] |         and vm.name == vm.features['template-name'] | ||||||
| 
 | 
 | ||||||
| # NOTE: Verifying RPMs this way is prone to TOCTOU. This is okay for local | def qrexec_popen(args, app, service, stdout=subprocess.PIPE, filter_esc=True): | ||||||
| # files, but may create problems if multiple instances of `qvm-template` are |     if args.updatevm: | ||||||
| # downloading the same file, so a lock is needed in that case. |         return app.domains[args.updatevm].run_service( | ||||||
|  |             service, | ||||||
|  |             filter_esc=filter_esc, | ||||||
|  |             stdout=stdout) | ||||||
|  |     return subprocess.Popen([ | ||||||
|  |             '/etc/qubes-rpc/%s' % service, | ||||||
|  |         ], | ||||||
|  |         stdin=subprocess.PIPE, | ||||||
|  |         stdout=stdout, | ||||||
|  |         stderr=subprocess.PIPE) | ||||||
|  | 
 | ||||||
|  | def qrexec_payload(args, app, spec, refresh): | ||||||
|  |     _ = app # unused | ||||||
|  | 
 | ||||||
|  |     # TODO: Check that spec != '---' | ||||||
|  | 
 | ||||||
|  |     def check_newline(string, name): | ||||||
|  |         if '\n' in string: | ||||||
|  |             parser.error(f"Malformed {name}:" + | ||||||
|  |                 " argument should not contain '\\n'.") | ||||||
|  | 
 | ||||||
|  |     payload = '' | ||||||
|  |     for repo in args.enablerepo if args.enablerepo else []: | ||||||
|  |         check_newline(repo, '--enablerepo') | ||||||
|  |         payload += '--enablerepo=%s\n' % repo | ||||||
|  |     for repo in args.disablerepo if args.disablerepo else []: | ||||||
|  |         check_newline(repo, '--disablerepo') | ||||||
|  |         payload += '--disablerepo=%s\n' % repo | ||||||
|  |     for repo in args.repoid if args.repoid else []: | ||||||
|  |         check_newline(repo, '--repoid') | ||||||
|  |         payload += '--repoid=%s\n' % repo | ||||||
|  |     if refresh: | ||||||
|  |         payload += '--refresh\n' | ||||||
|  |     check_newline(args.releasever, '--releasever') | ||||||
|  |     payload += '--releasever=%s\n' % args.releasever | ||||||
|  |     check_newline(spec, 'template name') | ||||||
|  |     payload += spec + '\n' | ||||||
|  |     payload += '---\n' | ||||||
|  |     for path in args.repo_files: | ||||||
|  |         with open(path, 'r') as fd: | ||||||
|  |             payload += fd.read() + '\n' | ||||||
|  |     return payload | ||||||
|  | 
 | ||||||
|  | def qrexec_repoquery(args, app, spec='*', refresh=False): | ||||||
|  |     proc = qrexec_popen(args, app, 'qubes.TemplateSearch') | ||||||
|  |     payload = qrexec_payload(args, app, spec, refresh) | ||||||
|  |     stdout, stderr = proc.communicate(payload.encode('UTF-8')) | ||||||
|  |     stdout = stdout.decode('ASCII') | ||||||
|  |     if proc.wait() != 0: | ||||||
|  |         for line in stderr.decode('ASCII').rstrip().split('\n'): | ||||||
|  |             print('[Qrexec] %s' % line, file=sys.stderr) | ||||||
|  |         raise ConnectionError("qrexec call 'qubes.TemplateSearch' failed.") | ||||||
|  |     name_re = re.compile(r'^[A-Za-z0-9._+\-]*$') | ||||||
|  |     evr_re = re.compile(r'^[A-Za-z0-9._+~]*$') | ||||||
|  |     date_re = re.compile(r'^\d+-\d+-\d+ \d+:\d+$') | ||||||
|  |     licence_re = re.compile(r'^[A-Za-z0-9._+\-()]*$') | ||||||
|  |     result = [] | ||||||
|  |     for line in stdout.split('|\n'): | ||||||
|  |         # Note that there's an empty entry at the end as .strip() is not used. | ||||||
|  |         # This is because if .strip() is used, the .split() will not work. | ||||||
|  |         if line == '': | ||||||
|  |             continue | ||||||
|  |         entry = line.split('|') | ||||||
|  |         try: | ||||||
|  |             # If there is an incorrect number of entries, raise an error | ||||||
|  |             # Unpack manually instead of stuffing into `Template` right away | ||||||
|  |             # so that it's easier to mutate stuff. | ||||||
|  |             name, epoch, version, release, reponame, dlsize, \ | ||||||
|  |                 buildtime, licence, url, summary, description = entry | ||||||
|  | 
 | ||||||
|  |             # Ignore packages that are not templates | ||||||
|  |             if not name.startswith(PACKAGE_NAME_PREFIX): | ||||||
|  |                 continue | ||||||
|  |             name = name[len(PACKAGE_NAME_PREFIX):] | ||||||
|  | 
 | ||||||
|  |             # Check that the values make sense | ||||||
|  |             if not re.fullmatch(name_re, name): | ||||||
|  |                 raise ValueError | ||||||
|  |             for val in [epoch, version, release]: | ||||||
|  |                 if not re.fullmatch(evr_re, val): | ||||||
|  |                     raise ValueError | ||||||
|  |             if not re.fullmatch(name_re, reponame): | ||||||
|  |                 raise ValueError | ||||||
|  |             dlsize = int(dlsize) | ||||||
|  |             # First verify that the date does not look weird, then parse it | ||||||
|  |             if not re.fullmatch(date_re, buildtime): | ||||||
|  |                 raise ValueError | ||||||
|  |             buildtime = datetime.datetime.strptime(buildtime, '%Y-%m-%d %H:%M') | ||||||
|  |             # XXX: Perhaps whitelist licenses directly? | ||||||
|  |             if not re.fullmatch(licence_re, licence): | ||||||
|  |                 raise ValueError | ||||||
|  |             # Check name actually matches spec | ||||||
|  |             if not is_match_spec(name, epoch, version, release, spec): | ||||||
|  |                 continue | ||||||
|  | 
 | ||||||
|  |             result.append(Template(name, epoch, version, release, reponame, | ||||||
|  |                 dlsize, buildtime, licence, url, summary, description)) | ||||||
|  |         except (TypeError, ValueError): | ||||||
|  |             raise ConnectionError(("qrexec call 'qubes.TemplateSearch' failed:" | ||||||
|  |                 " unexpected data format.")) | ||||||
|  |     return result | ||||||
|  | 
 | ||||||
|  | def qrexec_download(args, app, spec, path, dlsize=None, refresh=False): | ||||||
|  |     with open(path, 'wb') as fd: | ||||||
|  |         # Don't filter ESCs for binary files | ||||||
|  |         proc = qrexec_popen(args, app, 'qubes.TemplateDownload', | ||||||
|  |             stdout=fd, filter_esc=False) | ||||||
|  |         payload = qrexec_payload(args, app, spec, refresh) | ||||||
|  |         proc.stdin.write(payload.encode('UTF-8')) | ||||||
|  |         proc.stdin.close() | ||||||
|  |         with tqdm.tqdm(desc=spec, total=dlsize, unit_scale=True, | ||||||
|  |                 unit_divisor=1000, unit='B') as pbar: | ||||||
|  |             last = 0 | ||||||
|  |             while proc.poll() is None: | ||||||
|  |                 cur = fd.tell() | ||||||
|  |                 pbar.update(cur - last) | ||||||
|  |                 last = cur | ||||||
|  |                 time.sleep(0.1) | ||||||
|  |         if proc.wait() != 0: | ||||||
|  |             raise ConnectionError( | ||||||
|  |                 "qrexec call 'qubes.TemplateDownload' failed.") | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
| def verify_rpm(path, nogpgcheck=False, transaction_set=None): | def verify_rpm(path, nogpgcheck=False, transaction_set=None): | ||||||
|  |     # NOTE: Verifying RPMs this way is prone to TOCTOU. This is okay for local | ||||||
|  |     # files, but may create problems if multiple instances of `qvm-template` | ||||||
|  |     # are downloading the same file, so a lock is needed in that case. | ||||||
|     if transaction_set is None: |     if transaction_set is None: | ||||||
|         transaction_set = rpm.TransactionSet() |         transaction_set = rpm.TransactionSet() | ||||||
|     with open(path, 'rb') as fd: |     with open(path, 'rb') as fd: | ||||||
| @ -186,9 +336,119 @@ def extract_rpm(name, path, target): | |||||||
|         ], stdin=rpm2cpio.stdout, stdout=subprocess.DEVNULL) |         ], stdin=rpm2cpio.stdout, stdout=subprocess.DEVNULL) | ||||||
|     return rpm2cpio.wait() == 0 and cpio.wait() == 0 |     return rpm2cpio.wait() == 0 and cpio.wait() == 0 | ||||||
| 
 | 
 | ||||||
| def parse_config(path): | def get_dl_list(args, app, version_selector=VersionSelector.LATEST): | ||||||
|     with open(path, 'r') as fd: |     full_candid = {} | ||||||
|         return dict(line.rstrip('\n').split('=', 1) for line in fd) |     for template in args.templates: | ||||||
|  |         # This will be merged into `full_candid` later. | ||||||
|  |         # It is separated so that we can check whether it is empty. | ||||||
|  |         candid = {} | ||||||
|  | 
 | ||||||
|  |         # Skip local RPMs | ||||||
|  |         if template.endswith('.rpm'): | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         query_res = qrexec_repoquery(args, app, PACKAGE_NAME_PREFIX + template) | ||||||
|  | 
 | ||||||
|  |         # We only select one package for each distinct package name | ||||||
|  |         # TODO: Check local VM is managed by qvm-template | ||||||
|  |         for entry in query_res: | ||||||
|  |             ver = (entry.epoch, entry.version, entry.release) | ||||||
|  |             insert = False | ||||||
|  |             if version_selector == VersionSelector.LATEST: | ||||||
|  |                 if entry.name not in candid \ | ||||||
|  |                         or rpm.labelCompare(candid[entry.name][0], ver) < 0: | ||||||
|  |                     insert = True | ||||||
|  |             elif version_selector == VersionSelector.REINSTALL: | ||||||
|  |                 if entry.name not in app.domains: | ||||||
|  |                     parser.error( | ||||||
|  |                         "Template '%s' not already installed." % entry.name) | ||||||
|  |                 vm = app.domains[entry.name] | ||||||
|  |                 cur_ver = query_local_evr(vm) | ||||||
|  |                 if rpm.labelCompare(ver, cur_ver) == 0: | ||||||
|  |                     insert = True | ||||||
|  |             elif version_selector in [VersionSelector.LATEST_LOWER, | ||||||
|  |                     VersionSelector.LATEST_HIGHER]: | ||||||
|  |                 if entry.name not in app.domains: | ||||||
|  |                     parser.error( | ||||||
|  |                         "Template '%s' not already installed." % entry.name) | ||||||
|  |                 vm = app.domains[entry.name] | ||||||
|  |                 cur_ver = query_local_evr(vm) | ||||||
|  |                 cmp_res = -1 \ | ||||||
|  |                     if version_selector == VersionSelector.LATEST_LOWER \ | ||||||
|  |                     else 1 | ||||||
|  |                 if rpm.labelCompare(ver, cur_ver) == cmp_res: | ||||||
|  |                     if entry.name not in candid \ | ||||||
|  |                             or rpm.labelCompare(candid[entry.name][0], ver) < 0: | ||||||
|  |                         insert = True | ||||||
|  |             if insert: | ||||||
|  |                 candid[entry.name] = DlEntry(ver, entry.reponame, entry.dlsize) | ||||||
|  | 
 | ||||||
|  |         # XXX: As it's possible to include version information in `template` | ||||||
|  |         # Perhaps the messages can be improved | ||||||
|  |         if len(candid) == 0: | ||||||
|  |             if version_selector == VersionSelector.LATEST: | ||||||
|  |                 parser.error('Template \'%s\' not found.' % template) | ||||||
|  |             elif version_selector == VersionSelector.REINSTALL: | ||||||
|  |                 parser.error('Same version of template \'%s\' not found.' \ | ||||||
|  |                     % template) | ||||||
|  |             # Copy behavior of DNF and do nothing if version not found | ||||||
|  |             elif version_selector == VersionSelector.LATEST_LOWER: | ||||||
|  |                 print(("Template '%s' of lowest version" | ||||||
|  |                     " already installed, skipping..." % template), | ||||||
|  |                     file=sys.stderr) | ||||||
|  |             elif version_selector == VersionSelector.LATEST_HIGHER: | ||||||
|  |                 print(("Template '%s' of highest version" | ||||||
|  |                     " already installed, skipping..." % template), | ||||||
|  |                     file=sys.stderr) | ||||||
|  | 
 | ||||||
|  |         # Merge & choose the template with the highest version | ||||||
|  |         for name, entry in candid.items(): | ||||||
|  |             if name not in full_candid \ | ||||||
|  |                     or rpm.labelCompare(full_candid[name].evr, entry.evr) < 0: | ||||||
|  |                 full_candid[name] = entry | ||||||
|  | 
 | ||||||
|  |     return candid | ||||||
|  | 
 | ||||||
|  | def download(args, app, path_override=None, | ||||||
|  |         dl_list=None, suffix='', version_selector=VersionSelector.LATEST): | ||||||
|  |     if dl_list is None: | ||||||
|  |         dl_list = get_dl_list(args, app, version_selector=version_selector) | ||||||
|  | 
 | ||||||
|  |     path = path_override if path_override is not None else args.downloaddir | ||||||
|  |     for name, entry in dl_list.items(): | ||||||
|  |         version_str = build_version_str(entry.evr) | ||||||
|  |         spec = PACKAGE_NAME_PREFIX + name + '-' + version_str | ||||||
|  |         target = os.path.join(path, '%s.rpm' % spec) | ||||||
|  |         target_suffix = target + suffix | ||||||
|  |         if suffix != '' and os.path.exists(target_suffix): | ||||||
|  |             print('\'%s\' already exists, skipping...' % target, | ||||||
|  |                 file=sys.stderr) | ||||||
|  |         if os.path.exists(target): | ||||||
|  |             print('\'%s\' already exists, skipping...' % target, | ||||||
|  |                 file=sys.stderr) | ||||||
|  |             if suffix != '': | ||||||
|  |                 os.rename(target, target_suffix) | ||||||
|  |         else: | ||||||
|  |             print('Downloading \'%s\'...' % spec, file=sys.stderr) | ||||||
|  |             done = False | ||||||
|  |             for attempt in range(args.retries): | ||||||
|  |                 try: | ||||||
|  |                     qrexec_download(args, app, spec, target_suffix, | ||||||
|  |                         entry.dlsize) | ||||||
|  |                     done = True | ||||||
|  |                     break | ||||||
|  |                 except ConnectionError: | ||||||
|  |                     os.remove(target_suffix) | ||||||
|  |                     if attempt + 1 < args.retries: | ||||||
|  |                         print('\'%s\' download failed, retrying...' % spec, | ||||||
|  |                             file=sys.stderr) | ||||||
|  |                 except: | ||||||
|  |                     # Also remove file if interrupted by other means | ||||||
|  |                     os.remove(target_suffix) | ||||||
|  |                     raise | ||||||
|  |             if not done: | ||||||
|  |                 print('\'%s\' download failed.' % spec, file=sys.stderr) | ||||||
|  |                 sys.exit(1) | ||||||
| 
 | 
 | ||||||
| def install(args, app, version_selector=VersionSelector.LATEST, | def install(args, app, version_selector=VersionSelector.LATEST, | ||||||
|         override_existing=False): |         override_existing=False): | ||||||
| @ -339,156 +599,6 @@ def install(args, app, version_selector=VersionSelector.LATEST, | |||||||
|     finally: |     finally: | ||||||
|         os.remove(LOCK_FILE) |         os.remove(LOCK_FILE) | ||||||
| 
 | 
 | ||||||
| def qrexec_popen(args, app, service, stdout=subprocess.PIPE, filter_esc=True): |  | ||||||
|     if args.updatevm: |  | ||||||
|         return app.domains[args.updatevm].run_service( |  | ||||||
|             service, |  | ||||||
|             filter_esc=filter_esc, |  | ||||||
|             stdout=stdout) |  | ||||||
|     return subprocess.Popen([ |  | ||||||
|             '/etc/qubes-rpc/%s' % service, |  | ||||||
|         ], |  | ||||||
|         stdin=subprocess.PIPE, |  | ||||||
|         stdout=stdout, |  | ||||||
|         stderr=subprocess.PIPE) |  | ||||||
| 
 |  | ||||||
| def qrexec_payload(args, app, spec, refresh): |  | ||||||
|     _ = app # unused |  | ||||||
| 
 |  | ||||||
|     # TODO: Check that spec != '---' |  | ||||||
| 
 |  | ||||||
|     def check_newline(string, name): |  | ||||||
|         if '\n' in string: |  | ||||||
|             parser.error(f"Malformed {name}:" + |  | ||||||
|                 " argument should not contain '\\n'.") |  | ||||||
| 
 |  | ||||||
|     payload = '' |  | ||||||
|     for repo in args.enablerepo if args.enablerepo else []: |  | ||||||
|         check_newline(repo, '--enablerepo') |  | ||||||
|         payload += '--enablerepo=%s\n' % repo |  | ||||||
|     for repo in args.disablerepo if args.disablerepo else []: |  | ||||||
|         check_newline(repo, '--disablerepo') |  | ||||||
|         payload += '--disablerepo=%s\n' % repo |  | ||||||
|     for repo in args.repoid if args.repoid else []: |  | ||||||
|         check_newline(repo, '--repoid') |  | ||||||
|         payload += '--repoid=%s\n' % repo |  | ||||||
|     if refresh: |  | ||||||
|         payload += '--refresh\n' |  | ||||||
|     check_newline(args.releasever, '--releasever') |  | ||||||
|     payload += '--releasever=%s\n' % args.releasever |  | ||||||
|     check_newline(spec, 'template name') |  | ||||||
|     payload += spec + '\n' |  | ||||||
|     payload += '---\n' |  | ||||||
|     for path in args.repo_files: |  | ||||||
|         with open(path, 'r') as fd: |  | ||||||
|             payload += fd.read() + '\n' |  | ||||||
|     return payload |  | ||||||
| 
 |  | ||||||
| def qrexec_repoquery(args, app, spec='*', refresh=False): |  | ||||||
|     proc = qrexec_popen(args, app, 'qubes.TemplateSearch') |  | ||||||
|     payload = qrexec_payload(args, app, spec, refresh) |  | ||||||
|     stdout, stderr = proc.communicate(payload.encode('UTF-8')) |  | ||||||
|     stdout = stdout.decode('ASCII') |  | ||||||
|     if proc.wait() != 0: |  | ||||||
|         for line in stderr.decode('ASCII').rstrip().split('\n'): |  | ||||||
|             print('[Qrexec] %s' % line, file=sys.stderr) |  | ||||||
|         raise ConnectionError("qrexec call 'qubes.TemplateSearch' failed.") |  | ||||||
|     name_re = re.compile(r'^[A-Za-z0-9._+\-]*$') |  | ||||||
|     evr_re = re.compile(r'^[A-Za-z0-9._+~]*$') |  | ||||||
|     date_re = re.compile(r'^\d+-\d+-\d+ \d+:\d+$') |  | ||||||
|     licence_re = re.compile(r'^[A-Za-z0-9._+\-()]*$') |  | ||||||
|     result = [] |  | ||||||
|     for line in stdout.split('|\n'): |  | ||||||
|         # Note that there's an empty entry at the end as .strip() is not used. |  | ||||||
|         # This is because if .strip() is used, the .split() will not work. |  | ||||||
|         if line == '': |  | ||||||
|             continue |  | ||||||
|         entry = line.split('|') |  | ||||||
|         try: |  | ||||||
|             # If there is an incorrect number of entries, raise an error |  | ||||||
|             # Unpack manually instead of stuffing into `Template` right away |  | ||||||
|             # so that it's easier to mutate stuff. |  | ||||||
|             name, epoch, version, release, reponame, dlsize, \ |  | ||||||
|                 buildtime, licence, url, summary, description = entry |  | ||||||
| 
 |  | ||||||
|             # Ignore packages that are not templates |  | ||||||
|             if not name.startswith(PACKAGE_NAME_PREFIX): |  | ||||||
|                 continue |  | ||||||
|             name = name[len(PACKAGE_NAME_PREFIX):] |  | ||||||
| 
 |  | ||||||
|             # Check that the values make sense |  | ||||||
|             if not re.fullmatch(name_re, name): |  | ||||||
|                 raise ValueError |  | ||||||
|             for val in [epoch, version, release]: |  | ||||||
|                 if not re.fullmatch(evr_re, val): |  | ||||||
|                     raise ValueError |  | ||||||
|             if not re.fullmatch(name_re, reponame): |  | ||||||
|                 raise ValueError |  | ||||||
|             dlsize = int(dlsize) |  | ||||||
|             # First verify that the date does not look weird, then parse it |  | ||||||
|             if not re.fullmatch(date_re, buildtime): |  | ||||||
|                 raise ValueError |  | ||||||
|             buildtime = datetime.datetime.strptime(buildtime, '%Y-%m-%d %H:%M') |  | ||||||
|             # XXX: Perhaps whitelist licenses directly? |  | ||||||
|             if not re.fullmatch(licence_re, licence): |  | ||||||
|                 raise ValueError |  | ||||||
|             # Check name actually matches spec |  | ||||||
|             if not is_match_spec(name, epoch, version, release, spec): |  | ||||||
|                 continue |  | ||||||
| 
 |  | ||||||
|             result.append(Template(name, epoch, version, release, reponame, |  | ||||||
|                 dlsize, buildtime, licence, url, summary, description)) |  | ||||||
|         except (TypeError, ValueError): |  | ||||||
|             raise ConnectionError(("qrexec call 'qubes.TemplateSearch' failed:" |  | ||||||
|                 " unexpected data format.")) |  | ||||||
|     return result |  | ||||||
| 
 |  | ||||||
| def qrexec_download(args, app, spec, path, dlsize=None, refresh=False): |  | ||||||
|     with open(path, 'wb') as fd: |  | ||||||
|         # Don't filter ESCs for binary files |  | ||||||
|         proc = qrexec_popen(args, app, 'qubes.TemplateDownload', |  | ||||||
|             stdout=fd, filter_esc=False) |  | ||||||
|         payload = qrexec_payload(args, app, spec, refresh) |  | ||||||
|         proc.stdin.write(payload.encode('UTF-8')) |  | ||||||
|         proc.stdin.close() |  | ||||||
|         with tqdm.tqdm(desc=spec, total=dlsize, unit_scale=True, |  | ||||||
|                 unit_divisor=1000, unit='B') as pbar: |  | ||||||
|             last = 0 |  | ||||||
|             while proc.poll() is None: |  | ||||||
|                 cur = fd.tell() |  | ||||||
|                 pbar.update(cur - last) |  | ||||||
|                 last = cur |  | ||||||
|                 time.sleep(0.1) |  | ||||||
|         if proc.wait() != 0: |  | ||||||
|             raise ConnectionError( |  | ||||||
|                 "qrexec call 'qubes.TemplateDownload' failed.") |  | ||||||
|         return True |  | ||||||
| 
 |  | ||||||
| def build_version_str(evr): |  | ||||||
|     return '%s:%s-%s' % evr |  | ||||||
| 
 |  | ||||||
| def is_match_spec(name, epoch, version, release, spec): |  | ||||||
|     # Refer to "NEVRA Matching" in the DNF documentation |  | ||||||
|     # NOTE: Currently "arch" is ignored as the templates should be of "noarch" |  | ||||||
|     if epoch != 0: |  | ||||||
|         targets = [ |  | ||||||
|             f'{name}-{epoch}:{version}-{release}', |  | ||||||
|             f'{name}', |  | ||||||
|             f'{name}-{epoch}:{version}' |  | ||||||
|         ] |  | ||||||
|     else: |  | ||||||
|         targets = [ |  | ||||||
|             f'{name}-{epoch}:{version}-{release}', |  | ||||||
|             f'{name}-{version}-{release}', |  | ||||||
|             f'{name}', |  | ||||||
|             f'{name}-{epoch}:{version}', |  | ||||||
|             f'{name}-{version}' |  | ||||||
|         ] |  | ||||||
|     for prio, target in enumerate(targets): |  | ||||||
|         if fnmatch.fnmatch(target, spec): |  | ||||||
|             return True, prio |  | ||||||
|     return False, float('inf') |  | ||||||
| 
 |  | ||||||
| def list_templates(args, app, operation): | def list_templates(args, app, operation): | ||||||
|     tpl_list = [] |     tpl_list = [] | ||||||
| 
 | 
 | ||||||
| @ -664,120 +774,6 @@ def search(args, app): | |||||||
|             print('===', cur_header, '===') |             print('===', cur_header, '===') | ||||||
|         print(query_res[idx].name, ':', query_res[idx].summary) |         print(query_res[idx].name, ':', query_res[idx].summary) | ||||||
| 
 | 
 | ||||||
| def get_dl_list(args, app, version_selector=VersionSelector.LATEST): |  | ||||||
|     full_candid = {} |  | ||||||
|     for template in args.templates: |  | ||||||
|         # This will be merged into `full_candid` later. |  | ||||||
|         # It is separated so that we can check whether it is empty. |  | ||||||
|         candid = {} |  | ||||||
| 
 |  | ||||||
|         # Skip local RPMs |  | ||||||
|         if template.endswith('.rpm'): |  | ||||||
|             continue |  | ||||||
| 
 |  | ||||||
|         query_res = qrexec_repoquery(args, app, PACKAGE_NAME_PREFIX + template) |  | ||||||
| 
 |  | ||||||
|         # We only select one package for each distinct package name |  | ||||||
|         # TODO: Check local VM is managed by qvm-template |  | ||||||
|         for entry in query_res: |  | ||||||
|             ver = (entry.epoch, entry.version, entry.release) |  | ||||||
|             insert = False |  | ||||||
|             if version_selector == VersionSelector.LATEST: |  | ||||||
|                 if entry.name not in candid \ |  | ||||||
|                         or rpm.labelCompare(candid[entry.name][0], ver) < 0: |  | ||||||
|                     insert = True |  | ||||||
|             elif version_selector == VersionSelector.REINSTALL: |  | ||||||
|                 if entry.name not in app.domains: |  | ||||||
|                     parser.error( |  | ||||||
|                         "Template '%s' not already installed." % entry.name) |  | ||||||
|                 vm = app.domains[entry.name] |  | ||||||
|                 cur_ver = query_local_evr(vm) |  | ||||||
|                 if rpm.labelCompare(ver, cur_ver) == 0: |  | ||||||
|                     insert = True |  | ||||||
|             elif version_selector in [VersionSelector.LATEST_LOWER, |  | ||||||
|                     VersionSelector.LATEST_HIGHER]: |  | ||||||
|                 if entry.name not in app.domains: |  | ||||||
|                     parser.error( |  | ||||||
|                         "Template '%s' not already installed." % entry.name) |  | ||||||
|                 vm = app.domains[entry.name] |  | ||||||
|                 cur_ver = query_local_evr(vm) |  | ||||||
|                 cmp_res = -1 \ |  | ||||||
|                     if version_selector == VersionSelector.LATEST_LOWER \ |  | ||||||
|                     else 1 |  | ||||||
|                 if rpm.labelCompare(ver, cur_ver) == cmp_res: |  | ||||||
|                     if entry.name not in candid \ |  | ||||||
|                             or rpm.labelCompare(candid[entry.name][0], ver) < 0: |  | ||||||
|                         insert = True |  | ||||||
|             if insert: |  | ||||||
|                 candid[entry.name] = DlEntry(ver, entry.reponame, entry.dlsize) |  | ||||||
| 
 |  | ||||||
|         # XXX: As it's possible to include version information in `template` |  | ||||||
|         # Perhaps the messages can be improved |  | ||||||
|         if len(candid) == 0: |  | ||||||
|             if version_selector == VersionSelector.LATEST: |  | ||||||
|                 parser.error('Template \'%s\' not found.' % template) |  | ||||||
|             elif version_selector == VersionSelector.REINSTALL: |  | ||||||
|                 parser.error('Same version of template \'%s\' not found.' \ |  | ||||||
|                     % template) |  | ||||||
|             # Copy behavior of DNF and do nothing if version not found |  | ||||||
|             elif version_selector == VersionSelector.LATEST_LOWER: |  | ||||||
|                 print(("Template '%s' of lowest version" |  | ||||||
|                     " already installed, skipping..." % template), |  | ||||||
|                     file=sys.stderr) |  | ||||||
|             elif version_selector == VersionSelector.LATEST_HIGHER: |  | ||||||
|                 print(("Template '%s' of highest version" |  | ||||||
|                     " already installed, skipping..." % template), |  | ||||||
|                     file=sys.stderr) |  | ||||||
| 
 |  | ||||||
|         # Merge & choose the template with the highest version |  | ||||||
|         for name, entry in candid.items(): |  | ||||||
|             if name not in full_candid \ |  | ||||||
|                     or rpm.labelCompare(full_candid[name].evr, entry.evr) < 0: |  | ||||||
|                 full_candid[name] = entry |  | ||||||
| 
 |  | ||||||
|     return candid |  | ||||||
| 
 |  | ||||||
| def download(args, app, path_override=None, |  | ||||||
|         dl_list=None, suffix='', version_selector=VersionSelector.LATEST): |  | ||||||
|     if dl_list is None: |  | ||||||
|         dl_list = get_dl_list(args, app, version_selector=version_selector) |  | ||||||
| 
 |  | ||||||
|     path = path_override if path_override is not None else args.downloaddir |  | ||||||
|     for name, entry in dl_list.items(): |  | ||||||
|         version_str = build_version_str(entry.evr) |  | ||||||
|         spec = PACKAGE_NAME_PREFIX + name + '-' + version_str |  | ||||||
|         target = os.path.join(path, '%s.rpm' % spec) |  | ||||||
|         target_suffix = target + suffix |  | ||||||
|         if suffix != '' and os.path.exists(target_suffix): |  | ||||||
|             print('\'%s\' already exists, skipping...' % target, |  | ||||||
|                 file=sys.stderr) |  | ||||||
|         if os.path.exists(target): |  | ||||||
|             print('\'%s\' already exists, skipping...' % target, |  | ||||||
|                 file=sys.stderr) |  | ||||||
|             if suffix != '': |  | ||||||
|                 os.rename(target, target_suffix) |  | ||||||
|         else: |  | ||||||
|             print('Downloading \'%s\'...' % spec, file=sys.stderr) |  | ||||||
|             done = False |  | ||||||
|             for attempt in range(args.retries): |  | ||||||
|                 try: |  | ||||||
|                     qrexec_download(args, app, spec, target_suffix, |  | ||||||
|                         entry.dlsize) |  | ||||||
|                     done = True |  | ||||||
|                     break |  | ||||||
|                 except ConnectionError: |  | ||||||
|                     os.remove(target_suffix) |  | ||||||
|                     if attempt + 1 < args.retries: |  | ||||||
|                         print('\'%s\' download failed, retrying...' % spec, |  | ||||||
|                             file=sys.stderr) |  | ||||||
|                 except: |  | ||||||
|                     # Also remove file if interrupted by other means |  | ||||||
|                     os.remove(target_suffix) |  | ||||||
|                     raise |  | ||||||
|             if not done: |  | ||||||
|                 print('\'%s\' download failed.' % spec, file=sys.stderr) |  | ||||||
|                 sys.exit(1) |  | ||||||
| 
 |  | ||||||
| def remove(args, app): | def remove(args, app): | ||||||
|     _ = args, app # unused |     _ = args, app # unused | ||||||
| 
 | 
 | ||||||
| @ -804,7 +800,9 @@ def main(args=None, app=None): | |||||||
|     if args.refresh: |     if args.refresh: | ||||||
|         qrexec_repoquery(args, app, refresh=True) |         qrexec_repoquery(args, app, refresh=True) | ||||||
| 
 | 
 | ||||||
|     if args.operation == 'install': |     if args.operation == 'download': | ||||||
|  |         download(args, app) | ||||||
|  |     elif args.operation == 'install': | ||||||
|         install(args, app) |         install(args, app) | ||||||
|     elif args.operation == 'reinstall': |     elif args.operation == 'reinstall': | ||||||
|         install(args, app, version_selector=VersionSelector.REINSTALL, |         install(args, app, version_selector=VersionSelector.REINSTALL, | ||||||
| @ -821,8 +819,6 @@ def main(args=None, app=None): | |||||||
|         list_templates(args, app, 'info') |         list_templates(args, app, 'info') | ||||||
|     elif args.operation == 'search': |     elif args.operation == 'search': | ||||||
|         search(args, app) |         search(args, app) | ||||||
|     elif args.operation == 'download': |  | ||||||
|         download(args, app) |  | ||||||
|     elif args.operation == 'remove': |     elif args.operation == 'remove': | ||||||
|         remove(args, app) |         remove(args, app) | ||||||
|     elif args.operation == 'clean': |     elif args.operation == 'clean': | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 WillyPillow
						WillyPillow