示例#1
0
def check_ports():
    if utils.path_exists('./docker-compose.yml'):
        utils.info('Stopping services...')
        sh('{}docker-compose down --remove-orphans'.format(utils.optsudo()))

    ports = [
        utils.getenv(key, const.ENV_DEFAULTS[key]) for key in [
            const.HTTP_PORT_KEY,
            const.HTTPS_PORT_KEY,
            const.MDNS_PORT_KEY,
        ]
    ]

    utils.info('Checking ports...')
    retv = sh('sudo netstat -tulpn', capture=True)
    lines = retv.split('\n')

    used_ports = []
    used_lines = []
    for port in ports:
        for line in lines:
            if re.match(r'.*(:::|0.0.0.0:){}\s.*'.format(port), line):
                used_ports.append(port)
                used_lines.append(line)
                break

    if used_ports:
        utils.warn(
            'Port(s) {} already in use. '.format(', '.join(used_ports)) +
            "Run 'brewblox-ctl service ports' to configure Brewblox ports.")
        for line in used_lines:
            utils.warn(line)
        if not utils.confirm('Do you want to continue?'):
            raise SystemExit(1)
示例#2
0
def mset(data):
    with NamedTemporaryFile('w') as tmp:
        utils.show_data('datastore', data)
        json.dump(data, tmp)
        tmp.flush()
        sh(f'{const.CLI} http post --quiet {utils.datastore_url()}/mset -f {tmp.name}'
           )
示例#3
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')
示例#4
0
def migrate_ipv6_fix():
    # Undo disable-ipv6
    sh('sudo sed -i "/net.ipv6.*.disable_ipv6 = 1/d" /etc/sysctl.conf',
       check=False)

    # Enable ipv6 in docker daemon config
    utils.enable_ipv6()
示例#5
0
def editor(ctx, port):
    """Run web-based docker-compose.yml editor.

    This will start a new docker container listening on a host port (default: 8300).
    Navigate there in your browser to access the GUI for editing docker-compose.yml.

    When you're done editing, save your file in the GUI, and press Ctrl+C in the terminal.
    """
    utils.check_config()
    utils.confirm_mode()

    orig = utils.read_file('docker-compose.yml')

    sudo = utils.optsudo()
    host_ip = utils.host_ip()
    editor = 'brewblox/brewblox-web-editor:{}'.format(utils.docker_tag())

    utils.info('Pulling image...')
    sh('{}docker pull {}'.format(sudo, editor))

    try:
        utils.info('Starting editor...')
        sh('{}docker run'.format(sudo) + ' --rm --init' +
           ' -p "{}:8300"'.format(port) + ' -v "$(pwd):/app/config"' +
           ' {}'.format(editor) + ' --host-address {}'.format(host_ip) +
           ' --host-port {}'.format(port))
    except KeyboardInterrupt:  # pragma: no cover
        pass

    if orig != utils.read_file('docker-compose.yml'):
        utils.info('Configuration changes detected.')
        restart_services(ctx)
示例#6
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)
示例#7
0
def particle_wifi(dev: usb.core.Device):
    if utils.ctx_opts().dry_run:
        utils.info('Dry run: skipping activation of Spark listening mode')
    else:
        dev.reset()

        # Magic numbers for the USB control call
        HOST_TO_DEVICE = 0x40  # bmRequestType
        REQUEST_INIT = 1  # bRequest
        REQUEST_SEND = 3  # bRequest
        PARTICLE_LISTEN_INDEX = 70  # wIndex
        PARTICLE_LISTEN_VALUE = 0  # wValue
        PARTICLE_BUF_SIZE = 64  # wLength

        dev.ctrl_transfer(HOST_TO_DEVICE, REQUEST_INIT, PARTICLE_LISTEN_VALUE,
                          PARTICLE_LISTEN_INDEX, PARTICLE_BUF_SIZE)

        dev.ctrl_transfer(HOST_TO_DEVICE, REQUEST_SEND, PARTICLE_LISTEN_VALUE,
                          PARTICLE_LISTEN_INDEX, PARTICLE_BUF_SIZE)

    sleep(LISTEN_MODE_WAIT_S)

    serial = usb.util.get_string(dev, dev.iSerialNumber)
    path = next(
        Path('/dev/serial/by-id').glob(f'*{serial}*'), Path('/dev/ttyACM0'))

    utils.info('Press w to start Wifi configuration.')
    utils.info('Press Ctrl + ] to cancel.')
    utils.info('The Spark must be restarted after canceling.')
    sh(f'pyserial-miniterm -q {path.resolve()} 2>/dev/null')
示例#8
0
def apply_shared():
    """Apply docker-compose.shared.yml from data directory"""
    sh('cp -f {}/docker-compose.shared.yml ./'.format(const.CONFIG_DIR))
    shared_cfg = utils.read_shared_compose()
    usr_cfg = utils.read_compose()

    usr_cfg['version'] = shared_cfg['version']
    utils.write_compose(usr_cfg)
示例#9
0
def install_ctl_package(download: str = 'always'):  # always | missing | never
    exists = utils.path_exists('./brewblox-ctl.tar.gz')
    release = utils.getenv(const.CTL_RELEASE_KEY) or utils.getenv(
        const.RELEASE_KEY)
    if download == 'always' or download == 'missing' and not exists:
        sh(f'wget -q -O ./brewblox-ctl.tar.gz https://brewblox.blob.core.windows.net/ctl/{release}/brewblox-ctl.tar.gz'
           )
    sh('python3 -m pip install ./brewblox-ctl.tar.gz')
示例#10
0
def mset(data):
    with NamedTemporaryFile('w') as tmp:
        utils.show_data(data)
        json.dump(data, tmp)
        tmp.flush()
        sh('{} http post --quiet {}/mset -f {}'.format(const.CLI,
                                                       utils.datastore_url(),
                                                       tmp.name))
示例#11
0
def apply_config():
    """Apply system-defined configuration from config dir"""
    sh('cp -f {}/traefik-cert.yaml ./traefik/'.format(const.CONFIG_DIR))
    sh('cp -f {}/docker-compose.shared.yml ./'.format(const.CONFIG_DIR))
    shared_cfg = utils.read_shared_compose()
    usr_cfg = utils.read_compose()

    usr_cfg['version'] = shared_cfg['version']
    utils.write_compose(usr_cfg)
示例#12
0
def apply_config_files():
    """Apply system-defined configuration from config dir"""
    utils.info('Updating configuration files...')
    sh(f'cp -f {const.CONFIG_DIR}/traefik-cert.yaml ./traefik/')
    sh(f'cp -f {const.CONFIG_DIR}/docker-compose.shared.yml ./')
    shared_cfg = utils.read_shared_compose()
    usr_cfg = utils.read_compose()

    usr_cfg['version'] = shared_cfg['version']
    utils.write_compose(usr_cfg)
示例#13
0
def upped_migrate(prev_version):
    """Migration commands to be executed after the services have been started"""
    # Always run history configure
    history_url = utils.history_url()
    sh('{} http wait {}/ping'.format(const.CLI, history_url))
    sh('{} http post --quiet {}/configure'.format(const.CLI, history_url))

    if prev_version < StrictVersion('0.6.0'):
        utils.info('Migrating datastore from CouchDB to Redis...')
        datastore_migrate_redis()
示例#14
0
def downed_migrate(prev_version):
    """Migration commands to be executed without any running services"""
    if prev_version < StrictVersion('0.2.0'):
        # Breaking changes: Influx downsampling model overhaul
        # Old data is completely incompatible
        utils.select(
            'Upgrading to version >=0.2.0 requires a complete reset of your history data. '
            + "We'll be deleting it now")
        sh('sudo rm -rf ./influxdb')

    if prev_version < StrictVersion('0.3.0'):
        # Splitting compose configuration between docker-compose and docker-compose.shared.yml
        # Version pinning (0.2.2) will happen automatically
        utils.info('Moving system services to docker-compose.shared.yml...')
        config = utils.read_compose()
        sys_names = [
            'mdns',
            'eventbus',
            'influx',
            'datastore',
            'history',
            'ui',
            'traefik',
        ]
        usr_config = {
            'version': config['version'],
            'services': {
                key: svc
                for (key, svc) in config['services'].items()
                if key not in sys_names
            }
        }
        utils.write_compose(usr_config)

    if prev_version < StrictVersion('0.6.0'):
        # The datastore service is gone
        # Older services may still rely on it
        utils.info('Removing `depends_on` fields from docker-compose.yml...')
        config = utils.read_compose()
        for svc in config['services'].values():
            with suppress(KeyError):
                del svc['depends_on']
        utils.write_compose(config)

        # Init dir. It will be filled during upped_migrate
        utils.info('Creating redis/ dir...')
        sh('mkdir -p redis/')

    utils.info('Checking .env variables...')
    for (key, default_value) in const.ENV_DEFAULTS.items():
        current_value = utils.getenv(key)
        if current_value is None:
            utils.setenv(key, default_value)
示例#15
0
def migrate_compose_datastore():
    # The couchdb datastore service is gone
    # Older services may still rely on it
    utils.info('Removing `depends_on` fields from docker-compose.yml...')
    config = utils.read_compose()
    for svc in config['services'].values():
        with suppress(KeyError):
            del svc['depends_on']
    utils.write_compose(config)

    # Init dir. It will be filled during upped_migrate
    utils.info('Creating redis/ dir...')
    sh('mkdir -p redis/')
示例#16
0
def run_particle_flasher(release: str, pull: bool, cmd: str):
    tag = utils.docker_tag(release)
    sudo = utils.optsudo()

    opts = ' '.join([
        '-it',
        '--rm',
        '--privileged',
        '-v /dev:/dev',
        '--pull ' + ('always' if pull else 'missing'),
    ])

    sh(f'{sudo}docker-compose --log-level CRITICAL down', check=False)
    sh(f'{sudo}docker run {opts} brewblox/firmware-flasher:{tag} {cmd}')
示例#17
0
def discover_device(discovery, release):
    sudo = utils.optsudo()
    mdns = 'brewblox/brewblox-mdns:{}'.format(utils.docker_tag(release))

    utils.info('Pulling image...')
    sh('{}docker pull {}'.format(sudo, mdns), silent=True)

    utils.info('Discovering devices...')
    raw_devs = sh('{}docker run '.format(sudo) + '--rm -it ' + '--net=host ' +
                  '-v /dev/serial:/dev/serial ' +
                  '{} --cli --discovery {}'.format(mdns, discovery),
                  capture=True)

    return [dev for dev in raw_devs.split('\n') if dev.rstrip()]
示例#18
0
def run_esp_flasher(release: str, pull: bool):
    tag = utils.docker_tag(release)
    sudo = utils.optsudo()

    opts = ' '.join([
        '-it',
        '--rm',
        '--privileged',
        '-v /dev:/dev',
        '-w /app/firmware',
        '--entrypoint bash',
        '--pull ' + ('always' if pull else 'missing'),
    ])

    sh(f'{sudo}docker-compose --log-level CRITICAL down', check=False)
    sh(f'{sudo}docker run {opts} brewblox/brewblox-devcon-spark:{tag} flash')
示例#19
0
def makecert(dir, release: str = None):
    absdir = Path(dir).resolve()
    sudo = utils.optsudo()
    tag = utils.docker_tag(release)
    sh(f'mkdir -p "{absdir}"')
    sh(f'{sudo}docker run' + ' --rm --privileged' + ' --pull always' +
       f' -v "{absdir}":/certs/' + f' brewblox/omgwtfssl:{tag}')
    sh(f'sudo chmod 644 "{absdir}/brewblox.crt"')
    sh(f'sudo chmod 600 "{absdir}/brewblox.key"')
示例#20
0
def downed_migrate(prev_version):
    """Migration commands to be executed without any running services"""
    if prev_version < StrictVersion('0.2.0'):
        # Breaking changes: Influx downsampling model overhaul
        # Old data is completely incompatible
        utils.select(
            'Upgrading to version >=0.2.0 requires a complete reset of your history data. '
            + "We'll be deleting it now")
        sh('sudo rm -rf ./influxdb')

    if prev_version < StrictVersion('0.3.0'):
        # Splitting compose configuration between docker-compose and docker-compose.shared.yml
        # Version pinning (0.2.2) will happen automatically
        utils.info('Moving system services to docker-compose.shared.yml')
        config = utils.read_compose()
        sys_names = [
            'mdns',
            'eventbus',
            'influx',
            'datastore',
            'history',
            'ui',
            'traefik',
        ]
        usr_config = {
            'version': config['version'],
            'services': {
                key: svc
                for (key, svc) in config['services'].items()
                if key not in sys_names
            }
        }
        utils.write_compose(usr_config)

        utils.info('Writing env values for all variables')
        for key in [
                const.COMPOSE_FILES_KEY,
                const.RELEASE_KEY,
                const.HTTP_PORT_KEY,
                const.HTTPS_PORT_KEY,
                const.MDNS_PORT_KEY,
        ]:
            utils.setenv(key, utils.getenv(key, const.ENV_DEFAULTS[key]))
示例#21
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')
示例#22
0
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.'
        )
示例#23
0
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')
示例#24
0
def deploy_ctl_wrapper():
    sh(f'chmod +x "{const.SCRIPT_DIR}/brewblox-ctl"')
    if utils.user_home_exists():
        sh(f'mkdir -p "$HOME/.local/bin" && cp "{const.SCRIPT_DIR}/brewblox-ctl" "$HOME/.local/bin/"'
           )
    else:
        sh(f'sudo cp "{const.SCRIPT_DIR}/brewblox-ctl" /usr/local/bin/')
示例#25
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.')
示例#26
0
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')
示例#27
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 name in config['services'] and not force:
        click.echo(
            'Service "{}" already exists. Use the --force flag if you want to overwrite it'
            .format(name))
        raise SystemExit(1)

    config['services'][name] = {
        'image': 'brewblox/brewblox-plaato:${BREWBLOX_RELEASE}',
        'restart': 'unless-stopped',
        'environment': {
            'PLAATO_AUTH': token,
        },
        'command': '--name=' + name,
    }

    utils.write_compose(config)
    click.echo("Added Plaato service '{}'.".format(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('{}docker-compose up -d --remove-orphans'.format(sudo))
示例#28
0
def makecert(dir, release):
    """Generate a self-signed SSL certificate.

    \b
    Steps:
        - Create directory if it does not exist.
        - Create brewblox.crt and brewblox.key files.
    """
    utils.confirm_mode()
    sudo = utils.optsudo()
    tag = utils.docker_tag(release)
    absdir = Path(dir).absolute()
    sh(f'mkdir -p "{absdir}"')
    sh(f'{sudo}docker run' + ' --rm --privileged' + ' --pull always' +
       f' -v "{absdir}":/certs/' + f' brewblox/omgwtfssl:{tag}')
    sh(f'sudo chmod 644 "{absdir}/brewblox.crt"')
    sh(f'sudo chmod 600 "{absdir}/brewblox.key"')
示例#29
0
def _influx_line_count(service: str, args: str) -> Optional[int]:
    sudo = utils.optsudo()
    measurement = f'"brewblox"."downsample_1m"."{service}"'
    points_field = '"m_ Combined Influx points"'
    json_result = sh(
        f'{sudo}docker exec influxdb-migrate influx '
        '-database brewblox '
        f"-execute 'SELECT count({points_field}) FROM {measurement} {args}' "
        '-format json',
        capture=True)

    result = json.loads(json_result)

    try:
        return result['results'][0]['series'][0]['values'][0][1]
    except (IndexError, KeyError):
        return None
示例#30
0
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.'
            )