def check_init_opts(self): self.init_compose = True self.init_datastore = True self.init_history = True self.init_gateway = True self.init_eventbus = True if utils.path_exists('./docker-compose.yml'): self.init_compose = not utils.confirm( 'This directory already contains a docker-compose.yml file. ' + 'Do you want to keep it?') if utils.path_exists('./redis/'): self.init_datastore = not utils.confirm( 'This directory already contains Redis datastore files. ' + 'Do you want to keep them?') if utils.path_exists('./victoria/'): self.init_history = not utils.confirm( 'This directory already contains Victoria history files. ' + 'Do you want to keep them?') if utils.path_exists('./traefik/'): self.init_gateway = not utils.confirm( 'This directory already contains Traefik gateway files. ' + 'Do you want to keep them?') if utils.path_exists('./mosquitto/'): self.init_eventbus = not utils.confirm( 'This directory already contains Mosquitto config files. ' + 'Do you want to keep them?')
def test_confirm(mocked_ext): mocked_ext['input'].side_effect = [ '', 'flapjacks', 'NNNNO', 'YES', ] assert utils.confirm('what?') # default empty assert utils.confirm('why?') # yes assert mocked_ext['input'].call_count == 4
def check_confirm_opts(self): self.use_defaults = False self.skip_confirm = True self.use_defaults = utils.confirm( 'Do you want to install with default settings?') if not self.use_defaults: self.skip_confirm = utils.confirm( 'Do you want to disable the confirmation prompt for brewblox-ctl commands?' )
def check_lib(): if utils.is_brewblox_cwd() \ and not utils.path_exists('./brewblox_ctl_lib/__init__.py') \ and utils.confirm( 'brewblox-ctl requires extensions that match your Brewblox release. ' + 'Do you want to download them now?'): utils.load_ctl_lib(utils.ContextOpts(dry_run=False, verbose=True))
def check_ports(): if utils.path_exists('./docker-compose.yml'): utils.info('Stopping services...') sh(f'{utils.optsudo()}docker-compose down') ports = [ int(utils.getenv(key, const.ENV_DEFAULTS[key])) for key in [ const.HTTP_PORT_KEY, const.HTTPS_PORT_KEY, const.MQTT_PORT_KEY, ] ] try: port_connnections = [ conn for conn in psutil.net_connections() if conn.laddr.ip in ['::', '0.0.0.0'] and conn.laddr.port in ports ] except psutil.AccessDenied: utils.warn( 'Unable to read network connections. You need to run `netstat` or `lsof` manually.' ) port_connnections = [] if port_connnections: port_str = ', '.join( set(str(conn.laddr.port) for conn in port_connnections)) utils.warn(f'Port(s) {port_str} already in use.') utils.warn( 'Run `brewblox-ctl service ports` to configure Brewblox ports.') if not utils.confirm('Do you want to continue?'): raise SystemExit(1)
def add_node_red(force): """ Create a service for Node-RED. """ utils.check_config() utils.confirm_mode() name = 'node-red' sudo = utils.optsudo() host = utils.host_ip() port = utils.getenv(const.HTTPS_PORT_KEY) config = utils.read_compose() if not force: check_duplicate(config, name) config['services'][name] = { 'image': 'brewblox/node-red:${BREWBLOX_RELEASE}', 'restart': 'unless-stopped', 'volumes': [{ 'type': 'bind', 'source': f'./{name}', 'target': '/data', }] } sh(f'mkdir -p ./{name}') if [getgid(), getuid()] != [1000, 1000]: sh(f'sudo chown -R 1000:1000 ./{name}') utils.write_compose(config) click.echo(f'Added Node-RED service `{name}`.') if utils.confirm('Do you want to run `brewblox-ctl up` now?'): sh(f'{sudo}docker-compose up -d') click.echo(f'Visit https://{host}:{port}/{name} in your browser to load the editor.')
def add_plaato(name, token, force): """ Create a service for the Plaato airlock. This will periodically query the Plaato server for current state. An authentication token is required. See https://plaato.io/apps/help-center#!hc-auth-token on how to get one. """ utils.check_config() utils.confirm_mode() sudo = utils.optsudo() config = utils.read_compose() if not force: check_duplicate(config, name) config['services'][name] = { 'image': 'brewblox/brewblox-plaato:${BREWBLOX_RELEASE}', 'restart': 'unless-stopped', 'environment': { 'PLAATO_AUTH': token, }, 'command': f'--name={name}', } utils.write_compose(config) click.echo(f'Added Plaato service `{name}`.') click.echo('This service publishes history data, but does not have a UI component.') if utils.confirm('Do you want to run `brewblox-ctl up` now?'): sh(f'{sudo}docker-compose up -d')
def check_reboot_opts(self): self.reboot_needed = False self.prompt_reboot = True if self.docker_install \ or self.docker_group_add \ or utils.is_docker_user() and not utils.has_docker_rights(): self.reboot_needed = True self.prompt_reboot = utils.confirm( 'A reboot is required after installation. ' + 'Do you want to be prompted before that happens?')
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 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 add_tilt(force): """ Create a service for the Tilt hydrometer. The service listens for Bluetooth status updates from the Tilt, and requires the host to have a Bluetooth receiver. The empty ./tilt dir is created to hold calibration files. """ utils.check_config() utils.confirm_mode() name = 'tilt' sudo = utils.optsudo() config = utils.read_compose() if not force: check_duplicate(config, name) config['services'][name] = { 'image': 'brewblox/brewblox-tilt:${BREWBLOX_RELEASE}', 'restart': 'unless-stopped', 'privileged': True, 'network_mode': 'host', 'volumes': [{ 'type': 'bind', 'source': f'./{name}', 'target': '/share', }], 'labels': [ 'traefik.enable=false', ], } sh(f'mkdir -p ./{name}') utils.write_compose(config) click.echo(f'Added Tilt service `{name}`.') click.echo('It will automatically show up in the UI.\n') if utils.confirm('Do you want to run `brewblox-ctl up` now?'): sh(f'{sudo}docker-compose up -d')
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 main(args=sys.argv[1:]): try: ensure_tty() load_dotenv(Path('.env').resolve()) if utils.is_root(): click.secho('brewblox-ctl should not be run as root.', fg='red') raise SystemExit(1) if utils.is_armv6() \ and not utils.confirm( 'Raspberry Pi models 0 and 1 are not supported. Do you want to continue?', False): raise SystemExit(0) if sys.version_info[1] < SUPPORTED_PYTHON_MINOR: major = sys.version_info[0] minor = sys.version_info[1] click.echo( f'WARNING: You are using Python {major}.{minor}, which is no longer maintained.' ) click.echo('We recommend upgrading your system.') click.echo( 'For more information, please visit https://brewblox.netlify.app/user/system_upgrades.html' ) click.echo('') @click.group(cls=click_helpers.OrderedCommandCollection, sources=[ docker.cli, install.cli, env.cli, update.cli, http.cli, add_service.cli, service.cli, flash.cli, diagnostic.cli, fix.cli, database.cli, backup.cli, snapshot.cli, ]) @click.option('-y', '--yes', is_flag=True, envvar=const.SKIP_CONFIRM_KEY, help='Do not prompt to confirm commands.') @click.option( '-d', '--dry', '--dry-run', is_flag=True, help='Dry run mode: echo commands instead of running them.') @click.option('-q', '--quiet', is_flag=True, help='Show less detailed output.') @click.option('-v', '--verbose', is_flag=True, help='Show more detailed output.') @click.option('--color/--no-color', default=None, help='Format messages with unicode color codes.') @click.pass_context def cli(ctx, yes, dry, quiet, verbose, color): """ The Brewblox management tool. Example calls: \b brewblox-ctl install brewblox-ctl --quiet down brewblox-ctl --verbose up """ opts = ctx.ensure_object(utils.ContextOpts) opts.dry_run = dry opts.skip_confirm = yes opts.quiet = quiet opts.verbose = verbose opts.color = color cli(args=args, standalone_mode=False) except ClickException as ex: # pragma: no cover ex.show() escalate(ex) except Exception as ex: # pragma: no cover click.echo(str(ex), err=True) escalate(ex)
def main(args=sys.argv[1:]): try: load_dotenv(path.abspath('.env')) if utils.is_root(): click.echo('brewblox-ctl should not be run as root.') raise SystemExit(1) if utils.is_v6() \ and not utils.confirm( 'Raspberry Pi models 0 and 1 are not supported. Do you want to continue?', False): raise SystemExit(0) @click.group(cls=click_helpers.OrderedCommandCollection, sources=[ docker.cli, install.cli, env.cli, http.cli, *local_commands(), ]) @click.option('-y', '--yes', is_flag=True, envvar=const.SKIP_CONFIRM_KEY, help='Do not prompt to confirm commands.') @click.option( '-d', '--dry', '--dry-run', is_flag=True, help='Dry run mode: echo commands instead of running them.') @click.option('-q', '--quiet', is_flag=True, help='Show less detailed output.') @click.option('-v', '--verbose', is_flag=True, help='Show more detailed output.') @click.option('--color/--no-color', default=None, help='Format messages with unicode color codes.') @click.pass_context def cli(ctx, yes, dry, quiet, verbose, color): """ The Brewblox management tool. It can be used to create and control Brewblox configurations. More commands are available when used in a Brewblox installation directory. If the command you're looking for was not found, please check your current directory. By default, Brewblox is installed to ~/brewblox. Example calls: \b brewblox-ctl install brewblox-ctl --quiet down brewblox-ctl --verbose up """ opts = ctx.ensure_object(utils.ContextOpts) opts.dry_run = dry opts.skip_confirm = yes opts.quiet = quiet opts.verbose = verbose opts.color = color cli(args=args, standalone_mode=False) except UsageError as ex: ex.show() usage_hint(ex) escalate(ex) except ClickException as ex: # pragma: no cover ex.show() escalate(ex) except Exception as ex: # pragma: no cover click.echo(str(ex), err=True) escalate(ex)
def add_spark(name, discover_now, device_id, discovery_type, device_host, command, force, release, simulation): """ Create or update a Spark service. If you run brewblox-ctl add-spark without any arguments, it will prompt you for required info, and then create a sensibly configured service. If you want to fine-tune your service configuration, multiple arguments are available. For a detailed explanation: https://brewblox.netlify.com/user/connect_settings.html """ # utils.check_config() utils.confirm_mode() image_name = 'brewblox/brewblox-devcon-spark' sudo = utils.optsudo() config = utils.read_compose() if not force: check_duplicate(config, name) for (nm, svc) in config['services'].items(): img = svc.get('image', '') cmd = svc.get('command', '') if not any([ nm == name, not img.startswith(image_name), '--device-id' in cmd, '--device-host' in cmd, '--simulation' in cmd, ]): utils.warn(f'The existing Spark service `{nm}` does not have any connection settings.') utils.warn('It will connect to any controller it can find.') utils.warn('This may cause multiple services to connect to the same controller.') utils.warn(f'To reconfigure `{nm}`, please run:') utils.warn('') utils.warn(f' brewblox-ctl add-spark -f --name {nm}') utils.warn('') utils.select('Press ENTER to continue or Ctrl-C to exit') if discover_now and not simulation and not device_id: if device_host: dev = find_device_by_host(device_host) else: dev = choose_device(discovery_type, config) if dev: device_id = dev['id'] else: # We have no device ID, and no device host. Avoid a wildcard service click.echo('No valid combination of device ID and device host.') raise SystemExit(1) commands = [ '--name=' + name, '--discovery=' + discovery_type, ] if device_id: commands += ['--device-id=' + device_id] if device_host: commands += ['--device-host=' + device_host] if simulation: commands += ['--simulation'] if command: commands += [command] config['services'][name] = { 'image': f'{image_name}:{utils.docker_tag(release)}', 'privileged': True, 'restart': 'unless-stopped', 'command': ' '.join(commands) } if simulation: mount_dir = f'simulator__{name}' config['services'][name]['volumes'] = [{ 'type': 'bind', 'source': f'./{mount_dir}', 'target': '/app/simulator' }] sh(f'mkdir -m 777 -p {mount_dir}') utils.write_compose(config) click.echo(f'Added Spark service `{name}`.') click.echo('It will automatically show up in the UI.\n') if utils.confirm('Do you want to run `brewblox-ctl up` now?'): sh(f'{sudo}docker-compose up -d')
def check_duplicate(config: dict, name: str): if name in config['services'] \ and not utils.confirm(f'Service `{name}` already exists. Do you want to overwrite it?'): raise SystemExit(1)
def restart_services(ctx: click.Context, **kwargs): if utils.confirm('Do you want to restart your Brewblox services?'): ctx.invoke(up, **kwargs)
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.')