예제 #1
0
    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?')
예제 #2
0
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
예제 #3
0
    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?'
            )
예제 #4
0
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))
예제 #5
0
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)
예제 #6
0
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.')
예제 #7
0
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')
예제 #8
0
    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?')
예제 #9
0
    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}"?')
예제 #10
0
    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?')
예제 #11
0
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')
예제 #12
0
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())
예제 #13
0
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)
예제 #14
0
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)
예제 #15
0
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')
예제 #16
0
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)
예제 #17
0
def restart_services(ctx: click.Context, **kwargs):
    if utils.confirm('Do you want to restart your Brewblox services?'):
        ctx.invoke(up, **kwargs)
예제 #18
0
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.')