def add_user(self, username): UserManagement.validate_username(username) if not self._manage_users: return # Check if hte user already exists and exit. try: pwd.getpwnam(username) self._users.add(username) return except KeyError as ex: # Doesn't exist, fall through pass # If we're not allowed to manage users, error if not self._add_users: raise ValidationError( "User {} doesn't exist but is required by a DC/OS Component, and " "automatic user addition is disabled".format(username)) # Add the user: try: check_output([ 'useradd', '--system', '--home-dir', '/opt/mesosphere', '--shell', '/sbin/nologin', '-c', 'DCOS System User', username ]) self._users.add(username) except CalledProcessError as ex: raise ValidationError( "User {} doesn't exist and couldn't be created because of: {}". format(username, ex.output))
def add_user(self, username, groupname): UserManagement.validate_username(username) if not self._manage_users: return # Check if the user already exists and exit. try: if not is_windows: UserManagement.validate_user_group(username, groupname) self._users.add(username) return except KeyError as ex: # Doesn't exist, fall through log.warning("User [%s:%s] already exists", username, groupname) # If we're not allowed to manage users, error if not self._add_users: raise ValidationError( "User {} doesn't exist but is required by a DC/OS Component, and " "automatic user addition is disabled".format(username)) log.info("Add the user") add_user_cmd = [ 'useradd', '--system', '--home-dir', '/opt/mesosphere', '--shell', '/sbin/nologin', '-c', 'DCOS System User', ] # A group matching the username will be created by the adduser command. # Any other group that the user is added to needs to exist prior to executing the # adduser command. if groupname is not None and groupname != username: UserManagement.validate_group(groupname) add_user_cmd += ['-g', groupname] else: add_user_cmd += ['--user-group'] add_user_cmd += [username] try: log.debug(" ".join(add_user_cmd)) check_output(add_user_cmd) self._users.add(username) except CalledProcessError as ex: raise ValidationError( "User {} doesn't exist and couldn't be created because of: {}". format(username, ex.output))
def validate_compatible(packages, roles): # Every package name appears only once. names = set() ids = set() tuples = set() for package in packages: if package.name in names: raise ValidationError( "Repeated name {0} in set of packages {1}".format( package.name, ' '.join(map(lambda x: str(x.id), packages)))) if package.username is None and package.group is not None: raise ValidationError("`group` cannot be used without `username`") names.add(package.name) ids.add(str(package.id)) tuples.add((package.name, package.variant)) # All requires are met. # NOTE: Requires are given just to make it harder to accidentally # break a cluster. # Environment variables in packages, mapping from variable to package. environment = dict() for package in packages: # Check that all requirements of the package are met. # Requirements can be specified on a package name or full version string. for requirement in package.requires: name, variant = expand_require(requirement) if name not in names: raise ValidationError( ("Package {} variant {} requires {} variant {} but that " + "is not in the set of packages {}").format( package.id, package.variant, name, variant, ', '.join(str(x.id) for x in packages))) # No repeated/conflicting environment variables with other packages as # well as magic system environment variables. for k, v in package.environment.items(): if k in reserved_env_vars: raise ValidationError( "{0} are reserved environment vars and cannot be specified in packages. Present in package {1}" .format(", ".join(reserved_env_vars), package)) if k in environment: raise ValidationError( "Repeated environment variable {0}. In both packages {1} and {2}.".format( k, v, package)) environment[k] = package
def add_package_file(repository, package_filename): """Add a package to the repository from a file. repository: pkgpanda.Repository package_filename: location of the package file """ filename_suffix = '.tar.xz' # Extract Package Id (Filename must be path/{pkg-id}.tar.xz). name = os.path.basename(package_filename) if not name.endswith(filename_suffix): raise ValidationError( "ERROR: Can only add package tarballs which have names like " "{{pkg-id}}{}".format(filename_suffix)) pkg_id = name[:-len(filename_suffix)] # Validate the package id PackageId(pkg_id) def fetch(_, target): extract_tarball(package_filename, target) repository.add(fetch, pkg_id)
def validate_name(name): # [a-zA-Z0-9@._+-] # May not start with '.' or '-'. if not re.match(name_regex, name): raise ValidationError( "Invalid package name {0}. Must match the regex {1}".format( name, name_regex))
def remove_package(install, repository, package_id): """Remove a package from the local repository. Errors if any packages in package_ids are activated in install. install: pkgpanda.Install repository: pkgpanda.Repository package_id: package ID to remove from repository """ if package_id in install.get_active(): raise PackageConflict("Refusing to remove active package {0}".format(package_id)) sys.stdout.write("\rRemoving: {0}".format(package_id)) sys.stdout.flush() try: # Validate package id, that package is installed. PackageId(package_id) repository.remove(package_id) except ValidationError as ex: raise ValidationError("Invalid package id {0}".format(package_id)) from ex except OSError as ex: raise Exception("Error removing package {0}: {1}".format(package_id, ex)) from ex else: sys.stdout.write("\rRemoved: {0}".format(package_id)) finally: sys.stdout.write("\n") sys.stdout.flush()
def validate_version(version): # [a-zA-Z0-9@._+:] # May not contain a '-'. if not re.match(version_regex, version): raise ValidationError( "Invalid package version {0}. Must match the regex {1}".format( version, version_regex))
def symlink_tree(src, dest): for name in os.listdir(src): src_path = os.path.join(src, name) dest_path = os.path.join(dest, name) # Symlink files and symlinks directly. For directories make a # real directory and symlink everything inside. # NOTE: We could relax this and follow symlinks, but then we # need to be careful about recursive filesystem layouts. if os.path.isdir(src_path) and not os.path.islink(src_path): if os.path.exists(dest_path): # We can only merge a directory into a directory. # We won't merge into a symlink directory because that could # result in a package editing inside another package. if not os.path.isdir(dest_path) and not os.path.islink( dest_path): raise ValidationError( "Can't merge a file `{0}` and directory (or symlink) `{1}` with the same name." .format(src_path, dest_path)) else: os.makedirs(dest_path) # Recurse into the directory symlinking everything so long as the directory isn't symlink_tree(src_path, dest_path) else: try: os.symlink(src_path, dest_path) except FileNotFoundError as ex: raise ConflictingFile(src_path, dest_path, ex) from ex
def check_forbidden_services(path, services): """Check if package contains systemd services that may break DC/OS This functions checks the contents of systemd's unit file dirs and throws the exception if there are reserved services inside. Args: path: path where the package contents are services: list of reserved services to look for Raises: ValidationError: Reserved serice names were found inside the package """ services_dir_regexp = re.compile(r'dcos.target.wants(?:_.+)?') forbidden_srv_set = set(services) pkg_srv_set = set() for direntry in os.listdir(path): if not services_dir_regexp.match(direntry): continue pkg_srv_set.update(set(os.listdir(os.path.join(path, direntry)))) found_units = forbidden_srv_set.intersection(pkg_srv_set) if found_units: msg = "Reverved unit names found: " + ','.join(found_units) raise ValidationError(msg)
def validate_group_name(group_name): if not group_name: return if not re.match(linux_group_regex, group_name): raise ValidationError("Group {} has invalid name, must match the following regex: {}".format( group_name, linux_group_regex))
def swap_active_package(install, repository, package_id, systemd, block_systemd): """Replace an active package with a package_id with the same name. swap(install, repository, 'foo--version') will replace the active 'foo' package with 'foo--version'. install: pkgpanda.Install repository: pkgpanda.Repository package_id: package ID to activate systemd: start/stop systemd services block_systemd: if systemd, block waiting for systemd services to come up """ active = install.get_active() # TODO(cmaloney): I guarantee there is a better way to write this and # I've written the same logic before... packages_by_name = dict() for id_str in active: pkg_id = PackageId(id_str) packages_by_name[pkg_id.name] = pkg_id new_id = PackageId(package_id) if new_id.name not in packages_by_name: raise ValidationError("No package with name {} currently active to swap with.".format(new_id.name)) packages_by_name[new_id.name] = new_id new_active = list(map(str, packages_by_name.values())) # Activate with the new package name activate_packages(install, repository, new_active, systemd, block_systemd)
def checkout_to(self, directory): # fetch into a bare repository so if we're on a host which has a cache we can # only get the new commits. fetch_git(self.bare_folder, self.url) # Warn if the ref_origin is set and gives a different sha1 than the # current ref. try: origin_commit = get_git_sha1(self.bare_folder, self.ref_origin) except Exception as ex: raise ValidationError("Unable to find sha1 of ref_origin {}: {}".format(self.ref_origin, ex)) if self.ref != origin_commit: print( "WARNING: Current ref doesn't match the ref origin. " "Package ref should probably be updated to pick up " "new changes to the code:" + " Current: {}, Origin: {}".format(self.ref, origin_commit)) # Clone into `src/`. check_call(["git", "clone", "-q", self.bare_folder, directory]) # Checkout from the bare repo in the cache folder at the specific sha1 check_call([ "git", "--git-dir", directory + "/.git", "--work-tree", directory, "checkout", "-f", "-q", self.ref])
def get_git_sha1(bare_folder, ref): try: return check_output( ["git", "--git-dir", bare_folder, "rev-parse", ref + "^{commit}"]).decode('ascii').strip() except CalledProcessError as ex: raise ValidationError("Unable to find ref '{}' in '{}': {}".format( ref, bare_folder, ex)) from ex
def __init__(self, src_info, cache_dir): super().__init__(src_info) assert self.kind == 'git' if src_info.keys() != {'kind', 'git', 'ref', 'ref_origin'}: raise ValidationError( "git source must have keys 'git' (the repo to fetch), 'ref' (the sha-1 to " "checkout), and 'ref_origin' (the branch/tag ref was derived from)") if not is_sha(src_info['ref']): raise ValidationError("ref must be a sha1. Got: {}".format(src_info['ref'])) self.url = src_info['git'] self.ref = src_info['ref'] self.ref_origin = src_info['ref_origin'] self.bare_folder = cache_dir + "/cache.git".format()
def extract_archive(archive, dst_dir): archive_type = _identify_archive_type(archive) if archive_type == 'tar': check_call(["tar", "-xf", archive, "--strip-components=1", "-C", dst_dir]) elif archive_type == 'zip': check_call(["unzip", "-x", archive, "-d", dst_dir]) # unzip binary does not support '--strip-components=1', _strip_first_path_component(dst_dir) else: raise ValidationError("Unsupported archive: {}".format(os.path.basename(archive)))
def validate_group(group): # Empty group is allowed. if not group: return UserManagement.validate_group_name(group) try: grp.getgrnam(group) except KeyError: raise ValidationError("Group {} does not exist on the system".format(group))
def parse(id: str): parts = id.split('--') if len(parts) != 2: raise ValidationError( "Invalid package id {0}. Package ids may only ".format(id) + "contain one '--' which seperates the name and version") PackageId.validate_name(parts[0]) PackageId.validate_version(parts[1]) return parts[0], parts[1]
def _get_package_list(package_list_id: str, repository_url: str) -> List[str]: package_list_url = repository_url + '/package_lists/{}.package_list.json'.format(package_list_id) with tempfile.NamedTemporaryFile() as f: download(f.name, package_list_url, os.getcwd(), rm_on_error=False) package_list = load_json(f.name) if not isinstance(package_list, list): raise ValidationError('{} should contain a JSON list of packages. Got a {}'.format( package_list_url, type(package_list) )) return package_list
def __init__(self, src_info, cache_dir, working_directory): super().__init__(src_info) assert self.kind == 'git_local' if src_info.keys() > {'kind', 'rel_path'}: raise ValidationError( "Only kind, rel_path can be specified for git_local") if os.path.isabs(src_info['rel_path']): raise ValidationError( "rel_path must be a relative path to the current directory " "when used with git_local. Using a relative path means others " "that clone the repository will have things just work rather " "than a path.") self.src_repo_path = os.path.normpath(working_directory + '/' + src_info['rel_path']).rstrip('/') # Make sure there are no local changes, we can't `git clone` local changes. try: git_status = check_output([ 'git', '-C', self.src_repo_path, 'status', '--porcelain', '-uno', '-z' ]).decode() if len(git_status): raise ValidationError( "No local changse are allowed in the git_local_work base repository. " "Use `git -C {0} status` to see local changes. " "All local changes must be committed or stashed before the " "package can be built. One workflow (temporary commit): `git -C {0} " "commit -am TMP` to commit everything, build the package, " "`git -C {0} reset --soft HEAD^` to get back to where you were.\n\n" "Found changes: {1}".format(self.src_repo_path, git_status)) except CalledProcessError: raise ValidationError( "Unable to check status of git_local_work checkout {}. Is the " "rel_path correct?".format(src_info['rel_path'])) self.commit = get_git_sha1(self.src_repo_path + "/.git", "HEAD")
def expand_require(require): name = None variant = None if isinstance(require, str): name = require elif isinstance(require, dict): if 'name' not in require or 'variant' not in require: raise ValidationError( "When specifying a dependency in requires by dictionary to " + "depend on a variant both the name of the package and the " + "variant name must always be specified") name = require['name'] variant = require['variant'] if PackageId.is_id(name): raise ValidationError( "ERROR: Specifying a dependency on '" + name + "', an exact" + "package id isn't allowed. Dependencies may be specified by" + "package name alone or package name + variant (to change the" + "package variant).") return (name, variant)
def __init__(self, name, src_info, package_dir): super().__init__(name, src_info, package_dir) assert self.kind in {'url', 'url_extract'} if src_info.keys() != {'kind', 'sha1', 'url'}: raise ValidationError( "url and url_extract sources must have exactly 'sha1' (sha1 of the artifact" " which will be downloaded), and 'url' (url to download artifact) as options") self.url = src_info['url'] self.extract = (self.kind == 'url_extract') self.cache_filename = self._get_filename(package_dir + "/cache") self.sha = src_info['sha1']
def validate_user_group(username, group_name): user = pwd.getpwnam(username) if not group_name: return group = grp.getgrnam(group_name) if user.pw_gid != group.gr_gid: # check if the user is the right group, but the group is not primary. if username in group.gr_mem: return raise ValidationError( "User {} exists with current UID {}, however he should be assigned to group {} with {} UID, please " "check `buildinfo.json`".format(username, user.pw_gid, group_name, group.gr_gid))
def extract_archive(archive, dst_dir): archive_type = _identify_archive_type(archive) if archive_type == 'tar': if is_windows: check_call(["bsdtar", "-xf", archive, "-C", dst_dir]) else: check_call(["tar", "-xf", archive, "--strip-components=1", "-C", dst_dir]) elif archive_type == 'zip': if is_windows: check_call(["powershell.exe", "-command", "expand-archive", "-path", archive, "-destinationpath", dst_dir]) else: check_call(["unzip", "-x", archive, "-d", dst_dir]) # unzip binary does not support '--strip-components=1', _strip_first_path_component(dst_dir) else: raise ValidationError("Unsupported archive: {}".format(os.path.basename(archive)))
def _check_components_sanity(path): """Check if archive is sane Check if there is only one top level component (directory) in the extracted archive's directory. Args: path: path to the extracted archive's directory Raises: Raise an exception if there is anything else than a single directory """ dir_contents = os.listdir(path) if len(dir_contents) != 1 or not os.path.isdir(os.path.join(path, dir_contents[0])): raise ValidationError("Extracted archive has more than one top level" "component, unable to strip it.")
def get_src_fetcher(src_info, cache_dir, working_directory): try: kind = src_info['kind'] if kind not in pkgpanda.build.src_fetchers.all_fetchers: raise ValidationError( "No known way to catch src with kind '{}'. Known kinds: {}". format(kind, pkgpanda.src_fetchers.all_fetchers.keys())) args = {'src_info': src_info, 'cache_dir': cache_dir} if src_info['kind'] in ['git_local', 'url', 'url_extract']: args['working_directory'] = working_directory return pkgpanda.build.src_fetchers.all_fetchers[kind](**args) except ValidationError as ex: raise BuildError( "Validation error when fetching sources for package: {}".format( ex))
def __init__(self, src_info, cache_dir, working_directory): super().__init__(src_info) assert self.kind in {'url', 'url_extract'} if ('kind' not in src_info) or ('sha1' not in src_info) or ('url' not in src_info): raise ValidationError( "url and url_extract sources must have exactly 'sha1' (sha1 of the artifact" " which will be downloaded), and 'url' (url to download artifact) as options" ) self.url = src_info['url'] self.extract = (self.kind == 'url_extract') self.cache_dir = cache_dir self.cache_filename = self._get_filename(cache_dir) self.working_directory = working_directory self.sha = src_info['sha1']
def checkout_to(self, directory): # Download file to cache if it isn't already there if not os.path.exists(self.cache_filename): print("Downloading source tarball {}".format(self.url)) download_atomic(self.cache_filename, self.url, self.working_directory) # Validate the sha1 of the source is given and matches the sha1 file_sha = sha1(self.cache_filename) if self.sha != file_sha: corrupt_filename = self.cache_filename + '.corrupt' check_call(['mv', self.cache_filename, corrupt_filename]) raise ValidationError( "Provided sha1 didn't match sha1 of downloaded file, corrupt download saved as {}. " "Provided: {}, Download file's sha1: {}, Url: {}".format( corrupt_filename, self.sha, file_sha, self.url)) if self.extract: extract_archive(self.cache_filename, directory) else: # Copy the file(s) into src/ # TODO(cmaloney): Hardlink to save space? shutil.copyfile(self.cache_filename, self._get_filename(directory))
def fetcher(id, target): if repository_url is None: raise ValidationError("ERROR: Non-local package {} but no repository url given.".format(id)) return requests_fetcher(repository_url, id, target, os.getcwd())
def _do_bootstrap(install, repository): # These files should be set by the environment which initially builds # the host (cloud-init). repository_url = if_exists(load_string, install.get_config_filename("setup-flags/repository-url")) def fetcher(id, target): if repository_url is None: raise ValidationError("ERROR: Non-local package {} but no repository url given.".format(id)) return requests_fetcher(repository_url, id, target, os.getcwd()) setup_pkg_dir = install.get_config_filename("setup-packages") if os.path.exists(setup_pkg_dir): raise ValidationError( "setup-packages is no longer supported. It's functionality has been replaced with late " "binding packages. Found setup packages dir: {}".format(setup_pkg_dir)) setup_packages_to_activate = [] # If the host has late config values, build the late config package from them. late_config = if_exists(load_yaml, install.get_config_filename("setup-flags/late-config.yaml")) if late_config: pkg_id_str = late_config['late_bound_package_id'] late_values = late_config['bound_values'] print("Binding late config to late package {}".format(pkg_id_str)) print("Bound values: {}".format(late_values)) if not PackageId.is_id(pkg_id_str): raise ValidationError("Invalid late package id: {}".format(pkg_id_str)) pkg_id = PackageId(pkg_id_str) if pkg_id.version != "setup": raise ValidationError("Late package must have the version setup. Bad package: {}".format(pkg_id_str)) # Collect the late config package. with tempfile.NamedTemporaryFile() as f: download( f.name, repository_url + '/packages/{0}/{1}.dcos_config'.format(pkg_id.name, pkg_id_str), os.getcwd(), rm_on_error=False, ) late_package = load_yaml(f.name) # Resolve the late package using the bound late config values. final_late_package = resolve_late_package(late_package, late_values) # Render the package onto the filesystem and add it to the package # repository. with tempfile.NamedTemporaryFile() as f: do_gen_package(final_late_package, f.name) repository.add(lambda _, target: extract_tarball(f.name, target), pkg_id_str) setup_packages_to_activate.append(pkg_id_str) # If active.json is set on the host, use that as the set of packages to # activate. Otherwise just use the set of currently active packages (those # active in the bootstrap tarball) to_activate = None active_path = install.get_config_filename("setup-flags/active.json") if os.path.exists(active_path): print("Loaded active packages from", active_path) to_activate = load_json(active_path) # Ensure all packages are local print("Ensuring all packages in active set {} are local".format(",".join(to_activate))) for package in to_activate: repository.add(fetcher, package) else: print("Calculated active packages from bootstrap tarball") to_activate = list(install.get_active()) package_list_filename = install.get_config_filename("setup-flags/cluster-package-list") print("Checking for cluster packages in:", package_list_filename) package_list_id = if_exists(load_string, package_list_filename) if package_list_id: print("Cluster package list:", package_list_id) cluster_packages = _get_package_list(package_list_id, repository_url) print("Loading cluster-packages: {}".format(cluster_packages)) for package_id_str in cluster_packages: # Validate the package ids pkg_id = PackageId(package_id_str) # Fetch the packages if not local if not repository.has_package(package_id_str): repository.add(fetcher, package_id_str) # Add the package to the set to activate setup_packages_to_activate.append(package_id_str) else: print("No cluster-packages specified") # Calculate the full set of final packages (Explicit activations + setup packages). # De-duplicate using a set. to_activate = list(set(to_activate + setup_packages_to_activate)) print("Activating packages") install.activate(repository.load_packages(to_activate))
def _do_bootstrap(install, repository): # These files should be set by the environment which initially builds # the host (cloud-init). repository_url = if_exists( load_string, install.get_config_filename("setup-flags/repository-url")) # TODO(cmaloney): If there is 1+ master, grab the active config from a master. # If the config can't be grabbed from any of them, fail. def fetcher(id, target): if repository_url is None: raise ValidationError( "ERROR: Non-local package {} but no repository url given.". format(repository_url)) return requests_fetcher(repository_url, id, target, os.getcwd()) # Copy host/cluster-specific packages written to the filesystem manually # from the setup-packages folder into the repository. Do not overwrite or # merge existing packages, hard fail instead. setup_packages_to_activate = [] setup_pkg_dir = install.get_config_filename("setup-packages") copy_fetcher = partial(_copy_fetcher, setup_pkg_dir) if os.path.exists(setup_pkg_dir): for pkg_id_str in os.listdir(setup_pkg_dir): print("Installing setup package: {}".format(pkg_id_str)) if not PackageId.is_id(pkg_id_str): raise ValidationError( "Invalid package id in setup package: {}".format( pkg_id_str)) pkg_id = PackageId(pkg_id_str) if pkg_id.version != "setup": raise ValidationError( "Setup packages (those in `{0}`) must have the version setup. " "Bad package: {1}".format(setup_pkg_dir, pkg_id_str)) # Make sure there is no existing package if repository.has_package(pkg_id_str): print("WARNING: Ignoring already installed package {}".format( pkg_id_str)) repository.add(copy_fetcher, pkg_id_str) setup_packages_to_activate.append(pkg_id_str) # If active.json is set on the host, use that as the set of packages to # activate. Otherwise just use the set of currently active packages (those # active in the bootstrap tarball) to_activate = None active_path = install.get_config_filename("setup-flags/active.json") if os.path.exists(active_path): print("Loaded active packages from", active_path) to_activate = load_json(active_path) # Ensure all packages are local print("Ensuring all packages in active set {} are local".format( ",".join(to_activate))) for package in to_activate: repository.add(fetcher, package) else: print("Calculated active packages from bootstrap tarball") to_activate = list(install.get_active()) # Fetch and activate all requested additional packages to accompany the bootstrap packages. cluster_packages_filename = install.get_config_filename( "setup-flags/cluster-packages.json") cluster_packages = if_exists(load_json, cluster_packages_filename) print("Checking for cluster packages in:", cluster_packages_filename) if cluster_packages: if not isinstance(cluster_packages, list): print( 'ERROR: {} should contain a JSON list of packages. Got a {}' .format(cluster_packages_filename, type(cluster_packages))) print("Loading cluster-packages: {}".format(cluster_packages)) for package_id_str in cluster_packages: # Validate the package ids pkg_id = PackageId(package_id_str) # Fetch the packages if not local if not repository.has_package(package_id_str): repository.add(fetcher, package_id_str) # Add the package to the set to activate setup_packages_to_activate.append(package_id_str) else: print("No cluster-packages specified") # Calculate the full set of final packages (Explicit activations + setup packages). # De-duplicate using a set. to_activate = list(set(to_activate + setup_packages_to_activate)) print("Activating packages") install.activate(repository.load_packages(to_activate))