def _setup_networkd(self, *, executor: Executor) -> None: """Configure networkd and start it. Installs eth0 network configuration using ipv4. :param executor: Executor for target container. """ executor.create_file( destination=pathlib.Path("/etc/systemd/network/10-eth0.network"), content=dedent(""" [Match] Name=eth0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true """).encode(), file_mode="0644", ) executor.execute_run( command=["systemctl", "enable", "systemd-networkd"], check=True) executor.execute_run( command=["systemctl", "restart", "systemd-networkd"], check=True)
def _setup_apt(self, *, executor: Executor) -> None: """Configure apt & update cache. :param executor: Executor for target container. """ executor.execute_run(command=["apt-get", "update"], check=True) executor.execute_run(command=["apt-get", "install", "-y", "apt-utils"], check=True)
def _setup_snapcraft(*, executor: Executor) -> None: """Install Snapcraft in target environment. On Linux, the default behavior is to inject the host snap into the target environment. On other platforms, the Snapcraft snap is installed from the Snap Store. When installing the snap from the Store, we check if the user specifies a channel, using SNAPCRAFT_INSTALL_SNAP_CHANNEL=<channel>. If unspecified, we use the "stable" channel on the default track. On Linux, the user may specify this environment variable to force Snapcraft to install the snap from the Store rather than inject the host snap. :raises BaseConfigurationError: on error. """ # Requirement for apt gpg and version:git executor.execute_run( ["apt-get", "install", "-y", "gnupg", "dirmngr", "git"], capture_output=True, check=True, ) snap_channel = utils.get_managed_environment_snap_channel() if snap_channel is None and sys.platform != "linux": snap_channel = "stable" # Snaps that are already installed won't be reinstalled. # See https://github.com/canonical/craft-providers/issues/91 if snap_channel: try: snap_installer.install_from_store( executor=executor, snap_name="snapcraft", channel=snap_channel, classic=True, ) except snap_installer.SnapInstallationError as error: raise bases.BaseConfigurationError( "Failed to install snapcraft snap from store channel " f"{snap_channel!r} into target environment." ) from error else: try: snap_installer.inject_from_host( executor=executor, snap_name="snapcraft", classic=True ) except snap_installer.SnapInstallationError as error: raise bases.BaseConfigurationError( "Failed to inject host snapcraft snap into target environment." ) from error
def setup( self, *, executor: Executor, retry_wait: float = 0.25, timeout: Optional[float] = None, ) -> None: """Prepare base instance for use by the application. In addition to the guarantees provided by buildd: - charmcraft installed - python3 pip and setuptools installed :param executor: Executor for target container. :param retry_wait: Duration to sleep() between status checks (if required). :param timeout: Timeout in seconds. :raises BaseCompatibilityError: if instance is incompatible. :raises BaseConfigurationError: on other unexpected error. """ super().setup(executor=executor, retry_wait=retry_wait, timeout=timeout) try: # XXX Claudio 2021-07-22: craft-parts uses sudo, install it until # we adjust it to detect if changing to superuser is needed. executor.execute_run( [ "apt-get", "install", "-y", "sudo", ], check=True, capture_output=True, ) except subprocess.CalledProcessError as error: raise bases.BaseConfigurationError( brief="Failed to install the required dependencies.", ) from error try: snap_installer.inject_from_host( executor=executor, snap_name="charmcraft", classic=True ) except snap_installer.SnapInstallationError as error: raise bases.BaseConfigurationError( brief="Failed to inject host Charmcraft snap into target environment.", ) from error
def wait_for_system_ready(*, executor: Executor, retry_count=120, retry_interval: float = 0.5) -> None: """Wait until system is ready as defined by sysemctl is-system-running. :param executor: Executor for target container. :param retry_count: Number of times to check systemctl. :param retry_interval: Time between checks to systemctl. """ logger.info("Waiting for container to be ready...") for _ in range(retry_count): proc = executor.execute_run( command=["systemctl", "is-system-running"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, ) running_state = proc.stdout.decode().strip() if proc.returncode == 0: if running_state in ["running", "degraded"]: break logger.warning( "Unexpected state for systemctl is-system-running: %s", running_state, ) sleep(retry_interval) else: logger.warning("System exceeded timeout to get ready.")
def _setup_wait_for_system_ready(self, *, executor: Executor, retry_count=120, retry_interval: float = 0.5) -> None: """Wait until system is ready. :param executor: Executor for target container. :param timeout_secs: Timeout in seconds. """ logger.info("Waiting for container to be ready...") for _ in range(retry_count): proc = executor.execute_run( command=["systemctl", "is-system-running"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, ) running_state = proc.stdout.decode().strip() if running_state in ["running", "degraded"]: break logger.debug("systemctl is-system-running status: %s", running_state) sleep(retry_interval) else: logger.warning("System exceeded timeout to get ready.")
def is_target_file(*, executor: Executor, target: pathlib.Path) -> bool: """Check if path is file in executed environment. :param target: Path to check. :returns: True if file, False otherwise. """ proc = executor.execute_run(command=["test", "-f", target.as_posix()]) return proc.returncode == 0
def _read_os_release(self, *, executor: Executor) -> Optional[Dict[str, Any]]: try: proc = executor.execute_run( command=["cat", "/etc/os-release"], check=False, stdout=subprocess.PIPE, ) except subprocess.CalledProcessError: return None return parse_os_release(proc.stdout.decode())
def _read_craft_image_config( self, *, executor: Executor) -> Optional[Dict[str, Any]]: try: proc = executor.execute_run( command=["cat", "/etc/craft-image.conf"], check=True, stdout=subprocess.PIPE, ) except subprocess.CalledProcessError: return None return yaml.load(proc.stdout, Loader=yaml.SafeLoader)
def directory_sync_to_remote(*, executor: Executor, source: pathlib.Path, destination: pathlib.Path, delete=True, host_tar_cmd: str = "tar", target_tar_cmd: str = "tar") -> None: """Naive sync to remote using tarball. :param source: Host directory to copy. :param destination: Target destination directory to copy to. :param delete: Flag to delete existing destination, if exists. """ destination_path = destination.as_posix() if delete is True: executor.execute_run(["rm", "-rf", destination_path], check=True) executor.execute_run(["mkdir", "-p", destination_path], check=True) archive_proc = subprocess.Popen( [host_tar_cmd, "cpf", "-", "-C", str(source), "."], stdout=subprocess.PIPE, ) target_proc = executor.execute_popen( [target_tar_cmd, "xpvf", "-", "-C", destination_path], stdin=archive_proc.stdout, ) # Allow archive_proc to receive a SIGPIPE if destination_proc exits. if archive_proc.stdout: archive_proc.stdout.close() # Waot until done. target_proc.communicate()
def load(*, executor: Executor, config_path: pathlib.Path) -> Optional[Dict[str, Any]]: """Load craft configuration. :param executor: Executor for instance. :param config_path: Path to configuration file. """ try: proc = executor.execute_run( command=["cat", str(config_path)], check=True, stdout=subprocess.PIPE, ) except subprocess.CalledProcessError: return None return yaml.load(proc.stdout, Loader=yaml.SafeLoader)
def _setup_wait_for_network(self, *, executor: Executor, timeout_secs: int = 60) -> None: """Wait until networking is ready. :param executor: Executor for target container. :param timeout_secs: Timeout in seconds. """ logger.info("Waiting for networking to be ready...") for _ in range(timeout_secs * 2): proc = executor.execute_run( command=["getent", "hosts", "snapcraft.io"], stdout=subprocess.DEVNULL) if proc.returncode == 0: break sleep(0.5) else: logger.warning("Failed to setup networking.")
def _setup_wait_for_system_ready(self, *, executor: Executor, timeout_secs: int = 60) -> None: """Wait until system is ready. :param executor: Executor for target container. :param timeout_secs: Timeout in seconds. """ logger.info("Waiting for container to be ready...") for _ in range(timeout_secs * 2): proc = executor.execute_run( command=["systemctl", "is-system-running"], stdout=subprocess.PIPE) running_state = proc.stdout.decode().strip() if running_state in ["running", "degraded"]: break logger.debug("systemctl is-system-running: %s", running_state) sleep(0.5) else: logger.warning("Systemd failed to reach target before timeout.")
def _setup_resolved(self, *, executor: Executor) -> None: """Configure system-resolved to manage resolve.conf. :param executor: Executor for target container. :param timeout_secs: Timeout in seconds. """ executor.execute_run( command=[ "ln", "-sf", "/run/systemd/resolve/resolv.conf", "/etc/resolv.conf", ], check=True, ) executor.execute_run( command=["systemctl", "enable", "systemd-resolved"], check=True) executor.execute_run( command=["systemctl", "restart", "systemd-resolved"], check=True)
def _setup_snapd(self, *, executor: Executor) -> None: """Install snapd and dependencies and wait until ready. :param executor: Executor for target container. :param timeout_secs: Timeout in seconds. """ executor.execute_run( command=[ "apt-get", "install", "fuse", "udev", "--yes", ], check=True, ) executor.execute_run(command=["systemctl", "enable", "systemd-udevd"], check=True) executor.execute_run(command=["systemctl", "start", "systemd-udevd"], check=True) executor.execute_run(command=["apt-get", "install", "snapd", "--yes"], check=True) executor.execute_run(command=["systemctl", "start", "snapd.socket"], check=True) executor.execute_run(command=["systemctl", "start", "snapd.service"], check=True) executor.execute_run(command=["snap", "wait", "system", "seed.loaded"], check=True)