def test_command_exists(mocked_ext): m = mocked_ext['shutil.which'] m.return_value = '' assert not utils.command_exists('wobberjacky') m.return_value = '/bin/promised-land' assert utils.command_exists('pizza')
def disable_ssh_accept_env(): """Disable the 'AcceptEnv LANG LC_*' setting in sshd_config This setting is default on the Raspberry Pi, but leads to locale errors when an unsupported LANG is sent. Given that the Pi by default only includes the en_GB locale, the chances of being sent a unsupported locale are very real. """ file = Path('/etc/ssh/sshd_config') if not file.exists(): return content = file.read_text() updated = re.sub(r'^AcceptEnv LANG LC', '#AcceptEnv LANG LC', content, flags=re.MULTILINE) if content == updated: return with NamedTemporaryFile('w') as tmp: tmp.write(updated) tmp.flush() utils.info('Updating SSHD config to disable AcceptEnv...') utils.show_data('/etc/ssh/sshd_config', updated) sh(f'sudo chmod --reference={file} {tmp.name}') sh(f'sudo cp -fp {tmp.name} {file}') if utils.command_exists('systemctl'): utils.info('Restarting SSH service...') sh('sudo systemctl restart ssh')
def edit_avahi_config(): conf = Path(const.AVAHI_CONF) if not conf.exists(): return config = ConfigObj(str(conf), file_error=True) copy = deepcopy(config) config.setdefault('server', {}).setdefault('use-ipv6', 'no') config.setdefault('publish', {}).setdefault('publish-aaaa-on-ipv4', 'no') config.setdefault('reflector', {}).setdefault('enable-reflector', 'yes') if config == copy: return utils.show_data(conf, config.dict()) with NamedTemporaryFile('w') as tmp: config.filename = None lines = config.write() # avahi-daemon.conf requires a 'key=value' syntax tmp.write('\n'.join(lines).replace(' = ', '=') + '\n') tmp.flush() sh(f'sudo chmod --reference={conf} {tmp.name}') sh(f'sudo cp -fp {tmp.name} {conf}') if utils.command_exists('systemctl'): utils.info('Restarting avahi-daemon service...') sh('sudo systemctl restart avahi-daemon') else: utils.warn( '"systemctl" command not found. Please restart your machine to enable Wifi discovery.' )
def kill(zombies): """Stop and remove all containers on this host. This includes those not from Brewblox. If the --zombies flag is set, leftover processes that are still claiming a port will be forcibly removed. Use this if you get "port is already allocated" errors after your system crashed. """ utils.confirm_mode() sudo = utils.optsudo() sh(f'{sudo}docker rm --force $({sudo}docker ps -aq)', check=False) if zombies: # We can't use psutil for this, as we need root rights to get pids if not utils.command_exists('netstat'): utils.warn( 'Command `netstat` not found. Please install it by running:') utils.warn('') utils.warn( ' sudo apt-get update && sudo apt-get install net-tools') utils.warn('') return procs = re.findall(r'(\d+)/docker-proxy', sh('sudo netstat -pna', capture=True)) if procs: utils.info(f'Removing {len(procs)} zombies...') sh('sudo service docker stop') sh([f'sudo kill -9 {proc}' for proc in procs]) sh('sudo service docker start')
def add_particle_udev_rules(): rules_dir = '/etc/udev/rules.d' target = f'{rules_dir}/50-particle.rules' if not utils.path_exists(target) and utils.command_exists('udevadm'): utils.info('Adding udev rules for Particle devices...') sh(f'sudo mkdir -p {rules_dir}') sh(f'sudo cp {const.CONFIG_DIR}/50-particle.rules {target}') sh('sudo udevadm control --reload-rules && sudo udevadm trigger')
def check_system_opts(self): self.apt_install = True apt_deps = ' '.join(const.APT_DEPENDENCIES) if not utils.command_exists('apt-get'): utils.info( '`apt-get` is not available. You may need to find another way to install dependencies.' ) utils.info(f'Apt packages: "{apt_deps}"') self.apt_install = False elif not self.use_defaults: self.apt_install = utils.confirm( f'Do you want to install apt packages "{apt_deps}"?')
def fix_ipv6(config_file=None, restart=True): utils.info('Fixing Docker IPv6 settings...') if utils.is_wsl(): utils.info('WSL environment detected. Skipping IPv6 config changes.') return # Config is either provided, or parsed from active daemon process if not config_file: default_config_file = '/etc/docker/daemon.json' dockerd_proc = sh('ps aux | grep dockerd', capture=True) proc_match = re.match(r'.*--config-file[\s=](?P<file>.*\.json).*', dockerd_proc, flags=re.MULTILINE) config_file = proc_match and proc_match.group( 'file') or default_config_file utils.info(f'Using Docker config file {config_file}') # Read config. Create file if not exists sh(f"sudo touch '{config_file}'") config = sh(f"sudo cat '{config_file}'", capture=True) if 'fixed-cidr-v6' in config: utils.info('IPv6 settings are already present. Making no changes.') return # Edit and write. Do not overwrite existing values config = json.loads(config or '{}') config.setdefault('ipv6', False) config.setdefault('fixed-cidr-v6', '2001:db8:1::/64') config_str = json.dumps(config, indent=2) sh(f"echo '{config_str}' | sudo tee '{config_file}' > /dev/null") # Restart daemon if restart: if utils.command_exists('service'): utils.info('Restarting Docker service...') sh('sudo service docker restart') else: utils.warn( '"service" command not found. Please restart your machine to apply config changes.' )
def coredump(upload): """Read and upload a core dump file for the Spark 4. This requires the Spark to be connected over USB. Not compatible with the Spark 2 or 3. If the Spark 4 crashes, it stores what it was doing at the time of the crash. This command exports and uploads this data. The `esptool` python package is required, and will be installed if not found. """ if not utils.command_exists('esptool.py'): sh('python3 -m pip install esptool') sh('sudo -E env "PATH=$PATH" esptool.py --chip esp32 --baud 115200 read_flash 0xA10000 81920 coredump.bin') sh('base64 coredump.bin > coredump.b64') if upload: click.echo(utils.file_netcat('termbin.com', 9999, Path('./coredump.b64')).decode()) else: utils.info('Skipping upload. If you want to manually upload the file, run: ' + click.style('brewblox-ctl termbin ./coredump.b64', fg='green'))
def check_docker_opts(self): self.docker_install = True self.docker_group_add = True self.docker_pull = True if utils.command_exists('docker'): utils.info('Docker is already installed.') self.docker_install = False elif not self.use_defaults: self.docker_install = utils.confirm( 'Do you want to install docker?') if utils.is_docker_user(): user = utils.getenv('USER') utils.info(f'{user} already belongs to the docker group.') self.docker_group_add = False elif not self.use_defaults: self.docker_group_add = utils.confirm( 'Do you want to run docker commands without sudo?') if not self.use_defaults: self.docker_pull = utils.confirm( 'Do you want to pull the docker images for your services?')
def update_system_packages(): if utils.command_exists('apt-get'): utils.info('Updating apt packages...') sh('sudo apt-get update && sudo apt-get upgrade -y')
def install(use_defaults, apt_install, docker_install, docker_user, no_reboot, dir, release): """Create Brewblox directory; install system dependencies; reboot. Brewblox can be installed multiple times on the same computer. Settings and databases are stored in a Brewblox directory (default: ./brewblox). This command also installs system-wide dependencies (docker). After `brewblox-ctl install`, run `brewblox-ctl setup` in the created Brewblox directory. A reboot is required after installing docker, or adding the user to the 'docker' group. By default, `brewblox-ctl install` attempts to download packages using the apt package manager. If you are using a system without apt (eg. Synology NAS), this step will be skipped. You will need to manually install any missing libraries. \b Steps: - Install apt packages. - Install docker. - Add user to 'docker' group. - Create Brewblox directory (default ./brewblox). - Set variables in .env file. - Reboot. """ utils.confirm_mode() apt_deps = 'curl net-tools libssl-dev libffi-dev' user = utils.getenv('USER') default_dir = path.abspath('./brewblox') prompt_reboot = True if use_defaults is None: use_defaults = utils.confirm('Do you want to install with default settings?') # Check if packages should be installed if not utils.command_exists('apt'): utils.info('Apt is not available. You may need to find another way to install dependencies.') utils.info('Apt packages: "{}"'.format(apt_deps)) apt_install = False if apt_install is None: if use_defaults: apt_install = True else: apt_install = utils.confirm('Do you want to install apt packages "{}"?'.format(apt_deps)) # Check if docker should be installed if utils.command_exists('docker'): utils.info('Docker is already installed.') docker_install = False if docker_install is None: if use_defaults: docker_install = True else: docker_install = utils.confirm('Do you want to install docker?') # Check if user should be added to docker group if utils.is_docker_user(): utils.info('{} already belongs to the docker group.'.format(user)) docker_user = False if docker_user is None: if use_defaults: docker_user = True else: docker_user = utils.confirm('Do you want to run docker commands without sudo?') # Check used directory if dir is None: if use_defaults or utils.confirm("The default directory is '{}'. Do you want to continue?".format(default_dir)): dir = default_dir else: return if utils.path_exists(dir): if not utils.confirm('{} already exists. Do you want to continue?'.format(dir)): return if not no_reboot: prompt_reboot = utils.confirm('A reboot is required after installation. ' + 'Do you want to be prompted before that happens?') # Install Apt packages if apt_install: utils.info('Installing apt packages...') sh([ 'sudo apt update', 'sudo apt upgrade -y', 'sudo apt install -y {}'.format(apt_deps), ]) else: utils.info('Skipped: apt install.') # Install docker if docker_install: utils.info('Installing docker...') sh('curl -sL get.docker.com | sh') else: utils.info('Skipped: docker install.') # Add user to 'docker' group if docker_user: utils.info("Adding {} to 'docker' group...".format(user)) sh('sudo usermod -aG docker $USER') else: utils.info("Skipped: adding {} to 'docker' group.".format(user)) # Create install directory utils.info('Creating Brewblox directory ({})...'.format(dir)) sh('mkdir -p {}'.format(dir)) # Set variables in .env file utils.info('Setting variables in .env file...') dotenv_path = path.abspath('{}/.env'.format(dir)) sh('touch {}'.format(dotenv_path)) utils.setenv(const.RELEASE_KEY, release, dotenv_path) utils.setenv(const.CFG_VERSION_KEY, '0.0.0', dotenv_path) utils.setenv(const.SKIP_CONFIRM_KEY, str(use_defaults), dotenv_path) utils.info('Done!') # Reboot if not no_reboot: if prompt_reboot: utils.info('Press ENTER to reboot.') input() else: utils.info('Rebooting in 10 seconds...') sleep(10) sh('sudo reboot') else: utils.info('Skipped: reboot.')