def down(compose_args): """Stop all services. This wraps `docker-compose down` """ utils.check_config() utils.confirm_mode() sudo = utils.optsudo() sh(f'{sudo}docker-compose down ' + ' '.join(list(compose_args)))
def down(): """Stop all services. This wraps `docker-compose down --remove-orphans` """ utils.check_config() utils.confirm_mode() sudo = utils.optsudo() sh('{}docker-compose down --remove-orphans'.format(sudo))
def kill(): """Stop and remove all containers on this computer. This includes those not from Brewblox. """ utils.confirm_mode() sudo = utils.optsudo() sh('{}docker rm --force $({}docker ps -aq)'.format(sudo, sudo), check=False)
def read_fields(policy, measurement, keys): prefix = 'm_' * POLICIES.index(policy) fields = ','.join(['"{}{}"'.format(prefix, k) for k in keys]) utils.info('Reading {} {}'.format(measurement, policy)) sh('docker-compose exec influx influx -format csv ' + "-execute 'SELECT {} from brewblox.{}.\"{}\"'".format(fields, policy, measurement) + '> /tmp/influx_rename_{}.csv'.format(policy))
def up(compose_args): """Start all services. This wraps `docker-compose up -d` """ utils.check_config() utils.confirm_mode() sudo = utils.optsudo() sh(f'{sudo}docker-compose up -d ' + ' '.join(list(compose_args)))
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 prepare_flasher(release, pull): tag = utils.docker_tag(release) sudo = utils.optsudo() if pull: utils.info('Pulling flasher image...') sh('{}docker pull brewblox/firmware-flasher:{}'.format(sudo, tag)) if utils.path_exists('./docker-compose.yml'): utils.info('Stopping services...') sh('{}docker-compose down'.format(sudo))
def restart(): """Stop and start all services. This wraps `docker-compose down --remove-orphans; docker-compose up -d` Note: `docker-compose restart` also exists - it restarts containers without recreating them. """ utils.check_config() utils.confirm_mode() sudo = utils.optsudo() sh('{}docker-compose down --remove-orphans'.format(sudo)) sh('{}docker-compose up -d'.format(sudo))
def restart(compose_args): """Recreates all services. This wraps `docker-compose up -d --force-recreate` Note: `docker-compose restart` also exists - it restarts containers without recreating them. """ utils.check_config() utils.confirm_mode() sudo = utils.optsudo() sh(f'{sudo}docker-compose up -d --force-recreate ' + ' '.join(list(compose_args)))
def get_keys(measurement, pattern): retv = sh('docker-compose exec influx influx -format json ' + "-execute 'SHOW FIELD KEYS ON brewblox FROM brewblox.downsample_1m.\"{}\"'".format(measurement), capture=True) values = json.loads(retv)['results'][0]['series'][0]['values'] keys = [] for (k, t) in values: key = re.sub('m_', '', k, count=1) if re.match(pattern, key): keys.append(key) return keys
def follow(services): """Show logs for one or more services. This will start watching the logs for specified services. Call without arguments to show logs for all running services. Once started, press ctrl+C to stop. Service name will be equal to those specified in docker-compose.log, not the container name. To follow logs for service 'spark-one': \b GOOD: `brewblox-ctl follow spark-one` BAD: `brewblox-ctl follow brewblox_spark-one_1` """ utils.check_config() sudo = utils.optsudo() sh('{}docker-compose logs --follow {}'.format(sudo, ' '.join(services)))
def disable_ipv6(): """Disable IPv6 support on the host machine. Reason: https://github.com/docker/for-linux/issues/914 Should only be used if your services are having stability issues """ utils.confirm_mode() is_disabled = sh('cat /proc/sys/net/ipv6/conf/all/disable_ipv6', capture=True).strip() if is_disabled == '1': utils.info('IPv6 is already disabled') elif is_disabled == '0' or utils.ctx_opts().dry_run: utils.info('Disabling IPv6...') sh('echo "net.ipv6.conf.all.disable_ipv6 = 1" | sudo tee -a /etc/sysctl.conf') sh('echo "net.ipv6.conf.default.disable_ipv6 = 1" | sudo tee -a /etc/sysctl.conf') sh('echo "net.ipv6.conf.lo.disable_ipv6 = 1" | sudo tee -a /etc/sysctl.conf') sh('sudo sysctl -p') else: utils.info('Invalid result when checking IPv6 status: ' + is_disabled)
def save(file, force): """Save Brewblox directory to snapshot. This can be used to move Brewblox installations between hosts. To load the snapshot, use `brewblox-ctl install --snapshot ARCHIVE` or `brewblox-ctl snapshot load --file ARCHIVE` Block data stored on Spark controllers is not included in the snapshot. """ utils.check_config() utils.confirm_mode() dir = Path('./').resolve() if utils.path_exists(file): if force or utils.confirm(f'`{file}` already exists. ' + 'Do you want to overwrite it?'): sh(f'rm -f {file}') else: return sh(f'sudo tar -C {dir.parent} --exclude .venv -czf {file} {dir.name}') click.echo(Path(file).resolve())
def load(file): """Create Brewblox directory from snapshot. This can be used to move Brewblox installations between hosts. To create a snapshot, use `brewblox-ctl snapshot save` """ utils.check_config() utils.confirm_mode() dir = Path('./').resolve() with TemporaryDirectory() as tmpdir: utils.info(f'Extracting snapshot to {dir} directory...') sh(f'tar -xzf {file} -C {tmpdir}') content = list(Path(tmpdir).iterdir()) if utils.ctx_opts().dry_run: content = ['brewblox'] if len(content) != 1: raise ValueError(f'Multiple files found in snapshot: {content}') sh('sudo rm -rf ./*') # We need to explicitly include dotfiles in the mv glob src = content[0] sh(f'mv {src}/.[!.]* {src}/* {dir}/') actions.install_ctl_package(download='missing')
def write_fields(policy, keys, pattern, replace): prefix = 'm_' * POLICIES.index(policy) fields = [re.sub(pattern, replace, k, count=1) for k in keys] fields = [re.sub(r' ', r'\\ ', k) for k in fields] infile = '/tmp/influx_rename_{}.csv'.format(policy) outfile = '/tmp/influx_rename_{}.line'.format(policy) sh('rm {}'.format(outfile), check=False) with open(infile) as f_in: if not f_in.readline(): utils.info('No values found in policy "{}"'.format(policy)) return with open(outfile, 'w') as f_out: f_out.write('# DML\n') f_out.write('# CONTEXT-DATABASE: brewblox\n') f_out.write('# CONTEXT-RETENTION-POLICY: {}\n'.format(policy)) f_out.write('\n') while True: line = f_in.readline().strip() if not line: break values = line.split(',') measurement = values.pop(0) time = values.pop(0) data = ','.join(['{}{}={}'.format(prefix, field, value) for (field, value) in zip(fields, values) if value and value != '0']) if data: f_out.write('{} {} {}\n'.format(measurement, data, time)) utils.info('Writing {} {}'.format(measurement, policy)) sh('docker cp {} $(docker-compose ps -q influx):/rename'.format(outfile)) sh('docker-compose exec influx influx -import -path=/rename || true')
def test_sh(mocker): m_run = mocker.patch(TESTED + '.run') m_secho = mocker.patch(TESTED + '.click.secho') m_opts = mocker.patch(TESTED + '.ctx_opts').return_value m_opts.dry_run = False m_opts.verbose = False # Single call utils.sh('do things') assert m_secho.call_count == 0 assert m_run.call_count == 1 m_run.assert_called_with('do things', shell=True, check=True, universal_newlines=False, stdout=None, stderr=STDOUT) m_run.reset_mock() m_secho.reset_mock() # Unchecked call utils.sh('do naughty things', check=False) assert m_secho.call_count == 0 assert m_run.call_count == 1 m_run.assert_called_with('do naughty things', shell=True, check=False, universal_newlines=False, stdout=None, stderr=DEVNULL) m_run.reset_mock() m_secho.reset_mock() # Captured call utils.sh('gimme gimme', capture=True) assert m_secho.call_count == 0 assert m_run.call_count == 1 m_run.assert_called_with('gimme gimme', shell=True, check=True, universal_newlines=True, stdout=PIPE, stderr=STDOUT) m_run.reset_mock() m_secho.reset_mock() # Dry run utils.sh('invisible shenannigans', utils.ContextOpts(dry_run=True)) assert m_secho.call_count == 1 assert m_run.call_count == 0 m_run.reset_mock() m_secho.reset_mock() # List of commands utils.sh(['do this', 'and this', 'and that']) assert m_secho.call_count == 0 assert m_run.call_count == 3 m_run.reset_mock() m_secho.reset_mock() # Generator def generate(): yield 'first' yield 'second' m_run.reset_mock() utils.sh(generate(), utils.ContextOpts(verbose=True)) assert m_secho.call_count == 2 assert m_run.call_count == 2 m_run.assert_any_call('first', shell=True, check=True, universal_newlines=False, stdout=None, stderr=STDOUT) m_run.assert_called_with('second', shell=True, check=True, universal_newlines=False, stdout=None, stderr=STDOUT)
def run_flasher(release, args): tag = utils.docker_tag(release) sudo = utils.optsudo() opts = '-it --rm --privileged -v /dev:/dev' sh('{}docker run {} brewblox/firmware-flasher:{} {}'.format(sudo, opts, tag, args))
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.')
def install(ctx: click.Context, snapshot_file): """Install Brewblox and its dependencies. Brewblox can be installed multiple times on the same computer. Settings and databases are stored in a Brewblox directory. This command also installs system-wide dependencies. 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. When using the `--snapshot ARCHIVE` option, no dir is created. Instead, the directory in the snapshot is extracted. It will be renamed to the desired name of the Brewblox directory. \b Steps: - Ask confirmation for installation steps. - Install apt packages. - Install docker. - Add user to 'docker' group. - Fix host IPv6 settings. - Disable host-wide mDNS reflection. - Set variables in .env file. - If snapshot provided: - Load configuration from snapshot. - Else: - Check for port conflicts. - Create docker-compose configuration files. - Create datastore (Redis) directory. - Create history (Victoria) directory. - Create gateway (Traefik) directory. - Create SSL certificates. - Create eventbus (Mosquitto) directory. - Set version number in .env file. - Pull docker images. - Reboot if needed. """ utils.confirm_mode() user = utils.getenv('USER') opts = InstallOptions() opts.check_confirm_opts() opts.check_system_opts() opts.check_docker_opts() opts.check_reboot_opts() if not snapshot_file: opts.check_init_opts() # Install Apt packages if opts.apt_install: utils.info('Installing apt packages...') apt_deps = ' '.join(const.APT_DEPENDENCIES) sh([ 'sudo apt-get update', 'sudo apt-get upgrade -y', f'sudo apt-get install -y {apt_deps}', ]) else: utils.info('Skipped: apt-get install.') # Install docker if opts.docker_install: utils.info('Installing docker...') sh('curl -sL get.docker.com | sh', check=False) else: utils.info('Skipped: docker install.') # Add user to 'docker' group if opts.docker_group_add: utils.info(f"Adding {user} to 'docker' group...") sh('sudo usermod -aG docker $USER') else: utils.info(f"Skipped: adding {user} to 'docker' group.") # Always apply actions actions.disable_ssh_accept_env() actions.fix_ipv6(None, False) actions.edit_avahi_config() actions.add_particle_udev_rules() actions.uninstall_old_ctl_package() actions.deploy_ctl_wrapper() # Set variables in .env file # Set version number to 0.0.0 until snapshot load / init is done utils.info('Setting .env values...') utils.setenv(const.CFG_VERSION_KEY, '0.0.0') utils.setenv(const.SKIP_CONFIRM_KEY, str(opts.skip_confirm)) for key, default_val in const.ENV_DEFAULTS.items(): utils.setenv(key, utils.getenv(key, default_val)) # Install process splits here # Either load all config files from snapshot or run init sudo = utils.optsudo() if snapshot_file: ctx.invoke(snapshot.load, file=snapshot_file) else: release = utils.getenv('BREWBLOX_RELEASE') utils.info('Checking for port conflicts...') actions.check_ports() utils.info('Copying docker-compose.shared.yml...') sh(f'cp -f {const.CONFIG_DIR}/docker-compose.shared.yml ./') if opts.init_compose: utils.info('Copying docker-compose.yml...') sh(f'cp -f {const.CONFIG_DIR}/docker-compose.yml ./') # Stop after we're sure we have a compose file utils.info('Stopping services...') sh(f'{sudo}docker-compose down') if opts.init_datastore: utils.info('Creating datastore directory...') sh('sudo rm -rf ./redis/; mkdir ./redis/') if opts.init_history: utils.info('Creating history directory...') sh('sudo rm -rf ./victoria/; mkdir ./victoria/') if opts.init_gateway: utils.info('Creating gateway directory...') sh('sudo rm -rf ./traefik/; mkdir ./traefik/') utils.info('Creating SSL certificate...') actions.makecert('./traefik', release) if opts.init_eventbus: utils.info('Creating mosquitto config directory...') sh('sudo rm -rf ./mosquitto/; mkdir ./mosquitto/') # Always copy cert config to traefik dir sh(f'cp -f {const.CONFIG_DIR}/traefik-cert.yaml ./traefik/') # Init done - now set CFG version utils.setenv(const.CFG_VERSION_KEY, const.CURRENT_VERSION) if opts.docker_pull: utils.info('Pulling docker images...') sh(f'{sudo}docker-compose pull') utils.info('All done!') # Reboot if opts.reboot_needed: if opts.prompt_reboot: utils.info('Press ENTER to reboot.') input() else: utils.info('Rebooting in 10 seconds...') sleep(10) sh('sudo reboot')