示例#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 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()
示例#3
0
def ports(http, https):
    """Update used ports"""
    utils.check_config()
    utils.confirm_mode()

    cfg = {
        const.HTTP_PORT_KEY: http,
        const.HTTPS_PORT_KEY: https,
    }

    utils.info('Writing port settings to .env...')
    for key, val in cfg.items():
        utils.setenv(key, val)
示例#4
0
def remove(ctx, name):
    """Remove a service."""
    utils.check_config()
    utils.confirm_mode()

    config = utils.read_compose()
    try:
        del config['services'][name]
        utils.info("Removing service '{}'".format(name))
        utils.write_compose(config)
        restart_services(ctx)
    except KeyError:
        click.echo("Service '{}' not found".format(name))
示例#5
0
def discover_spark(discovery_type):
    """
    Discover available Spark controllers.

    This prints device ID for all devices, and IP address for Wifi devices.
    If a device is connected over USB, and has Wifi active, it may show up twice.

    Multicast DNS (mDNS) is used for Wifi discovery.
    Whether this works is dependent on the configuration of your router and avahi-daemon.
    """
    for dev in discover_device(discovery_type):
        utils.info(dev['desc'])
    utils.info('Done!')
示例#6
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/')
示例#7
0
def discover_spark(discovery, release):
    """
    Discover available Spark controllers.

    This prints device ID for all devices, and IP address for Wifi devices.
    If a device is connected over USB, and has Wifi active, it may show up twice.

    Multicast DNS (mDNS) is used for Wifi discovery. Whether this works is dependent on your router's configuration.
    """
    utils.confirm_mode()
    for dev in discover_device(discovery, release):
        click.echo(dev)
    utils.info('Done!')
示例#8
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()]
示例#9
0
def remove(ctx, services):
    """Remove a service."""
    utils.check_config()
    utils.confirm_mode()

    config = utils.read_compose()
    for name in services:
        try:
            del config['services'][name]
            utils.info(f"Removed service '{name}'")
        except KeyError:
            utils.warn(f"Service '{name}' not found")

    if services:
        utils.write_compose(config)
        restart_services(ctx, compose_args=['--remove-orphans'])
示例#10
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)
示例#11
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]))
示例#12
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)
示例#13
0
def migrate_compose_split():
    # 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)
示例#14
0
def _copy_influx_measurement(
    service: str,
    date: str,
    duration: str,
    target: str,
    offset: int = 0,
):
    """
    Export measurement from Influx, and copy/import to `target`.
    This requires an InfluxDB docker container with name 'influxdb-migrate'
    to have been started.
    """
    QUERY_BATCH_SIZE = 5000
    FILE_BATCH_SIZE = 50000
    FILE_DIR = './influxdb-export'
    sudo = utils.optsudo()
    measurement = f'"brewblox"."downsample_1m"."{service}"'
    args = f'where time > now() - {duration}' if duration else ''

    total_lines = _influx_line_count(service, args)
    offset = max(offset, 0)
    offset -= (offset % QUERY_BATCH_SIZE
               )  # Round down to multiple of batch size
    num_lines = offset

    if target == 'file':
        sh(f'mkdir -p {FILE_DIR}')

    if total_lines is None:
        return

    while True:
        generator = utils.sh_stream(
            f'{sudo}docker exec influxdb-migrate influx '
            '-database brewblox '
            f"-execute 'SELECT * FROM {measurement} {args} ORDER BY time LIMIT {QUERY_BATCH_SIZE} OFFSET {offset}' "
            '-format csv')

        headers = next(generator, '').strip()
        time = None

        if not headers:
            return

        fields = [
            f[2:].replace(' ', '\\ ')  # Remove 'm_' prefix and escape spaces
            for f in headers.split(',')[2:]  # Ignore 'name' and 'time' columns
        ]

        with NamedTemporaryFile('w') as tmp:
            for line in generator:
                if not line:
                    continue

                num_lines += 1
                values = line.strip().split(',')
                name = values[0]
                time = values[1]

                # Influx line protocol:
                # MEASUREMENT k1=1,k2=2,k3=3 TIMESTAMP
                tmp.write(f'{name} ')
                tmp.write(','.join(
                    (f'{f}={v}' for f, v in zip(fields, values[2:]) if v)))
                tmp.write(f' {time}\n')

            tmp.flush()

            if target == 'victoria':
                with open(tmp.name, 'rb') as rtmp:
                    url = f'{utils.host_url()}/victoria/write'
                    urllib3.disable_warnings()
                    requests.get(url, data=rtmp, verify=False)

            elif target == 'file':
                idx = str(offset // FILE_BATCH_SIZE + 1).rjust(3, '0')
                fname = f'{FILE_DIR}/{service}__{date}__{duration or "all"}__{idx}.lines'
                sh(f'cat "{tmp.name}" >> "{fname}"')

            else:
                raise ValueError(f'Invalid target: {target}')

        offset = 0
        args = f'where time > {time}'
        utils.info(f'{service}: exported {num_lines}/{total_lines} lines')
示例#15
0
def log(add_compose, upload):
    """Generate and share log file for bug reports.

    This command generates a comprehensive report on current system state and logs.
    When reporting bugs, a termbin blink to the output is often the first thing asked for.

    For best results, run when the services are still active.
    Service logs are discarded after `brewblox-ctl down`.

    Care is taken to prevent accidental leaks of confidential information.
    Only known variables are read from .env,
    and the `--no-add-compose` flag allows skipping compose configuration.
    The latter is useful if the configuration contains passwords or tokens.

    To review or edit the output, use the `--no-upload` flag.
    The output will include instructions on how to manually upload the file.

    \b
    Steps:
        - Create ./brewblox.log file.
        - Append Brewblox .env variables.
        - Append service logs.
        - Append content of docker-compose.yml (optional).
        - Append content of docker-compose.shared.yml (optional).
        - Append blocks from Spark services.
        - Upload file to termbin.com for shareable link (optional).
    """
    utils.check_config()
    utils.confirm_mode()
    sudo = utils.optsudo()

    # Create log
    utils.info('Log file: {}'.format(path.abspath('./brewblox.log')))
    sh('echo "BREWBLOX DIAGNOSTIC DUMP" > brewblox.log')
    sh('date >> brewblox.log')

    # Add .env values
    utils.info('Writing Brewblox .env values...')
    sh('echo "==============VARS==============" >> brewblox.log')
    sh('echo "$(uname -a)" >> brewblox.log')
    sh('echo "$({}docker --version)" >> brewblox.log'.format(sudo))
    sh('echo "$({}docker-compose --version)" >> brewblox.log'.format(sudo))
    sh('echo "{}={}" >> brewblox.log'.format(key, utils.getenv(key)) for key in ENV_KEYS)

    # Add service logs
    utils.info('Writing service logs...')
    sh('echo "==============LOGS==============" >> brewblox.log')
    try:
        config_names = list(utils.read_compose()['services'].keys())
        shared_names = list(utils.read_shared_compose()['services'].keys())
        names = [n for n in config_names if n not in shared_names] + shared_names
        raw_cmd = '{}docker-compose logs --timestamps --no-color --tail 200 {} >> brewblox.log; ' + \
            "echo '\\n' >> brewblox.log"
        sh(raw_cmd.format(sudo, name) for name in names)
    except Exception as ex:
        sh('echo {} >> brewblox.log'.format(shlex.quote(type(ex).__name__ + ': ' + str(ex))))

    # Add compose config
    if add_compose:
        utils.info('Writing docker-compose configuration...')
        sh('echo "==============COMPOSE==============" >> brewblox.log')
        sh('cat docker-compose.yml >> brewblox.log')
        sh('echo "==============SHARED===============" >> brewblox.log')
        sh('cat docker-compose.shared.yml >> brewblox.log')
    else:
        utils.info('Skipping docker-compose configuration...')

    # Add blocks
    utils.info('Writing Spark blocks...')
    sh('echo "==============BLOCKS==============" >> brewblox.log')
    host_url = utils.host_url()
    services = utils.list_services('brewblox/brewblox-devcon-spark')
    query = '{} http get --pretty {}/{}/objects >> brewblox.log || echo "{} not found" >> brewblox.log'
    sh(query.format(const.CLI, host_url, svc, svc) for svc in services)

    # Upload
    if upload:
        utils.info('Uploading brewblox.log to termbin.com...')
        sh('cat brewblox.log | nc termbin.com 9999')
    else:
        utils.info('Skipping upload. If you want to manually upload the log, run: ' +
                   click.style('cat brewblox.log | nc termbin.com 9999', fg='green'))
示例#16
0
def update(ctx, update_ctl, update_ctl_done, pull, avahi_config, migrate,
           prune, from_version):
    """Download and apply updates.

    This is the one-stop-shop for updating your Brewblox install.
    You can use any of the options to fine-tune the update by enabling or disabling subroutines.

    By default, all options are enabled.

    --update-ctl/--no-update-ctl determines whether it download new versions
    of brewblox-ctl and brewblox-ctl lib. If this flag is set, update will download the new version
    and then restart itself. This way, the migrate is done with the latest version of brewblox-ctl.

    If you're using dry run mode, you'll notice the hidden option --update-ctl-done.
    You can use it to watch the rest of the update: it\'s a flag to avoid endless loops.

    --pull/--no-pull governs whether new docker images are pulled.
    This is useful if any of your services is using a local image (not from Docker Hub).

    --avahi-config/--no-avahi-config. Check avahi-daemon configuration.
    This is required for TCP discovery of Spark controllers.

    --migrate/--no-migrate. Updates regularly require changes to configuration.
    To do this, services are stopped. If the update only requires pulling docker images,
    you can disable migration to avoid the docker-compose down/up.

    --prune/--no-prune (prompts if not set). Updates to docker images can leave unused old versions
    on your system. These can be pruned to free up disk space.
    Do note that this includes all images on your system, not just those created by Brewblox.

    \b
    Steps:
        - Update brewblox-ctl and extensions.
        - Restart update command to run with updated brewblox-ctl.
        - Pull docker images.
        - Stop services.
        - Migrate configuration files.
        - Copy docker-compose.shared.yml from defaults.
        - Start services.
        - Migrate service configuration.
        - Write version number to .env file.
        - Prune unused images.
    """
    utils.check_config()
    utils.confirm_mode()
    sudo = utils.optsudo()

    prev_version = StrictVersion(from_version)

    if prev_version.version == (0, 0, 0):
        click.echo(
            'This configuration was never set up. Please run brewblox-ctl setup first'
        )
        raise SystemExit(1)

    if prev_version > StrictVersion(const.CURRENT_VERSION):
        click.echo(
            'Your system is running a version newer than the selected release. '
            + 'This may be due to switching release tracks.' +
            'You can use the --from-version flag if you know what you are doing.'
        )
        raise SystemExit(1)

    if Path.home().name != 'root' and Path.home().exists() \
            and Path('/usr/local/bin/brewblox-ctl').exists():  # pragma: no cover
        utils.warn('brewblox-ctl appears to have been installed using sudo.')
        if utils.confirm('Do you want to fix this now?'):
            sh('sudo {} -m pip uninstall -y brewblox-ctl docker-compose'.
               format(const.PY),
               check=False)
            utils.pip_install('brewblox-ctl')  # docker-compose is a dependency

            # Debian stretch still has the bug where ~/.local/bin is not included in $PATH
            if '.local/bin' not in utils.getenv('PATH'):
                sh('echo \'export PATH="$HOME/.local/bin:$PATH"\' >> ~/.bashrc'
                   )

            utils.info(
                'Please run "exec $SHELL --login" to apply the changes to $PATH'
            )
            return

    if update_ctl and not update_ctl_done:
        utils.info('Updating brewblox-ctl...')
        utils.pip_install('brewblox-ctl')
        utils.info('Updating brewblox-ctl libs...')
        utils.load_ctl_lib()
        # Restart ctl - we just replaced the source code
        sh(' '.join([
            const.PY, *const.ARGS, '--update-ctl-done',
            '--prune' if prune else '--no-prune'
        ]))
        return

    if avahi_config:
        utils.update_avahi_config()

    if migrate:
        # Everything except downed_migrate can be done with running services
        utils.info('Stopping services...')
        sh('{}docker-compose down --remove-orphans'.format(sudo))

        utils.info('Migrating configuration files...')
        apply_config()
        downed_migrate(prev_version)
    else:
        utils.info('Updating configuration files...')
        apply_config()

    if pull:
        utils.info('Pulling docker images...')
        sh('{}docker-compose pull'.format(sudo))

    utils.info('Starting services...')
    sh('{}docker-compose up -d --remove-orphans'.format(sudo))

    if migrate:
        utils.info('Migrating service configuration...')
        upped_migrate(prev_version)

        utils.info('Updating version number to {}...'.format(
            const.CURRENT_VERSION))
        utils.setenv(const.CFG_VERSION_KEY, const.CURRENT_VERSION)

    if prune:
        utils.info('Pruning unused images...')
        sh('{}docker image prune -f'.format(sudo))
        utils.info('Pruning unused volumes...')
        sh('{}docker volume prune -f'.format(sudo))
示例#17
0
def save(save_compose, ignore_spark_error):
    """Create a backup of Brewblox settings.

    A zip archive containing JSON/YAML files is created in the ./backup/ directory.
    The archive name will include current date and time to ensure uniqueness.

    The backup is not exported to any kind of remote/cloud storage.

    To use this command in scripts, run it as `brewblox-ctl --quiet backup save`.
    Its only output to stdout will be the absolute path to the created backup.

    The command will fail if any of the Spark services could not be contacted.

    As it does not make any destructive changes to configuration,
    this command is not affected by --dry-run.

    \b
    Stored data:
    - .env
    - docker-compose.yml.   (Optional)
    - Datastore databases.
    - Spark service blocks.

    \b
    NOT stored:
    - History data.

    """
    utils.check_config()
    urllib3.disable_warnings()

    file = 'backup/brewblox_backup_{}.zip'.format(
        datetime.now().strftime('%Y%m%d_%H%M'))
    with suppress(FileExistsError):
        mkdir(path.abspath('backup/'))

    url = utils.datastore_url()

    utils.info('Waiting for the datastore...')
    http.wait(url, info_updates=True)
    resp = requests.get(url + '/_all_dbs', verify=False)
    resp.raise_for_status()
    dbs = [v for v in resp.json() if not v.startswith('_')]

    config = utils.read_compose()
    sparks = [
        k for k, v in config['services'].items()
        if v.get('image', '').startswith('brewblox/brewblox-devcon-spark')
    ]
    zipf = zipfile.ZipFile(file, 'w', zipfile.ZIP_DEFLATED)

    utils.info('Exporting .env')
    zipf.write('.env')

    if save_compose:
        utils.info('Exporting docker-compose.yml')
        zipf.write('docker-compose.yml')

    utils.info('Exporting databases: {}'.format(', '.join(dbs)))
    for db in dbs:
        resp = requests.get('{}/{}/_all_docs'.format(url, db),
                            params={'include_docs': True},
                            verify=False)
        resp.raise_for_status()
        docs = [v['doc'] for v in resp.json()['rows']]
        for d in docs:
            del d['_rev']
        zipf.writestr(db + '.datastore.json', json.dumps(docs))

    for spark in sparks:
        utils.info("Exporting Spark blocks from '{}'".format(spark))
        resp = requests.get('{}/{}/export_objects'.format(
            utils.host_url(), spark),
                            verify=False)
        try:
            resp.raise_for_status()
            zipf.writestr(spark + '.spark.json', resp.text)
        except Exception as ex:
            if ignore_spark_error:
                utils.info("Skipping '{}' due to error: {}".format(
                    spark, str(ex)))
            else:
                raise ex

    zipf.close()
    click.echo(path.abspath(file))
    utils.info('Done!')
示例#18
0
def load(archive, load_env, load_compose, load_datastore, load_spark, update):
    """Load and apply Brewblox settings backup.

    This function uses files generated by `brewblox-ctl backup save` as input.
    You can use the --load-XXXX options to partially load the backup.

    This does not attempt to merge data: it will overwrite current docker-compose.yml,
    datastore databases, and Spark blocks.

    Blocks on Spark services not in the backup file will not be affected.

    If dry-run is enabled, it will echo all configuration from the backup archive.

    Steps:
        - Write .env
        - Read .env values
        - Write docker-compose.yml, run `docker-compose up`.
        - Write all datastore files found in backup.
        - Write all Spark blocks found in backup.
        - Run brewblox-ctl update
    """
    utils.check_config()
    utils.confirm_mode()
    urllib3.disable_warnings()

    sudo = utils.optsudo()
    host_url = utils.host_url()
    store_url = utils.datastore_url()

    zipf = zipfile.ZipFile(archive, 'r', zipfile.ZIP_DEFLATED)
    available = zipf.namelist()
    datastore_files = [v for v in available if v.endswith('.datastore.json')]
    spark_files = [v for v in available if v.endswith('.spark.json')]

    if load_env and '.env' in available:
        with NamedTemporaryFile('w') as tmp:
            data = zipf.read('.env').decode()
            utils.info('Writing .env')
            utils.show_data(data)
            tmp.write(data)
            tmp.flush()
            sh('cp -f {} .env'.format(tmp.name))

        utils.info('Reading .env values')
        load_dotenv(path.abspath('.env'))

    if load_compose:
        if 'docker-compose.yml' in available:
            utils.info('Writing docker-compose.yml')
            utils.write_compose(yaml.safe_load(
                zipf.read('docker-compose.yml')))
            sh('{} docker-compose up -d --remove-orphans'.format(sudo))
        else:
            utils.info('docker-compose.yml file not found in backup archive')

    if load_datastore:
        if datastore_files:
            utils.info('Waiting for the datastore...')
            sh('{} http wait {}'.format(const.CLI, store_url))
        else:
            utils.info('No datastore files found in backup archive')

        for f in datastore_files:
            db = f[:-len('.datastore.json')]

            utils.info('Recreating database {}'.format(db))
            sh('{} http delete {}/{} --allow-fail'.format(
                const.CLI, store_url, db))
            sh('{} http put {}/{}'.format(const.CLI, store_url, db))

            utils.info('Writing database {}'.format(db))
            with NamedTemporaryFile('w') as tmp:
                data = {'docs': json.loads(zipf.read(f).decode())}
                utils.show_data(data)
                json.dump(data, tmp)
                tmp.flush()
                sh('{} http post {}/{}/_bulk_docs -f {}'.format(
                    const.CLI, store_url, db, tmp.name))

    if load_spark:
        sudo = utils.optsudo()

        if not spark_files:
            utils.info('No Spark files found in backup archive')

        for f in spark_files:
            spark = f[:-len('.spark.json')]
            utils.info('Writing blocks to Spark service {}'.format(spark))
            with NamedTemporaryFile('w') as tmp:
                data = json.loads(zipf.read(f).decode())
                utils.show_data(data)
                json.dump(data, tmp)
                tmp.flush()
                sh('{} http post {}/{}/import_objects -f {}'.format(
                    const.CLI, host_url, spark, tmp.name))
                sh('{} docker-compose restart {}'.format(sudo, spark))

    zipf.close()

    if update:
        utils.info('Updating brewblox...')
        sh('{} update'.format(const.CLI))

    utils.info('Done!')
示例#19
0
def save(save_compose, ignore_spark_error):
    """Create a backup of Brewblox settings.

    A zip archive containing JSON/YAML files is created in the ./backup/ directory.
    The archive name will include current date and time to ensure uniqueness.

    The backup is not exported to any kind of remote/cloud storage.

    To use this command in scripts, run it as `brewblox-ctl --quiet backup save`.
    Its only output to stdout will be the absolute path to the created backup.

    The command will fail if any of the Spark services could not be contacted.

    As it does not make any destructive changes to configuration,
    this command is not affected by --dry-run.

    \b
    Stored data:
    - .env
    - docker-compose.yml.   (Optional)
    - Datastore databases.
    - Spark service blocks.
    - Node-RED data.

    \b
    NOT stored:
    - History data.

    """
    utils.check_config()
    urllib3.disable_warnings()

    file = 'backup/brewblox_backup_{}.zip'.format(datetime.now().strftime('%Y%m%d_%H%M'))
    with suppress(FileExistsError):
        mkdir(path.abspath('backup/'))

    store_url = utils.datastore_url()

    utils.info('Waiting for the datastore...')
    http.wait(store_url + '/ping', info_updates=True)

    config = utils.read_compose()
    sparks = [
        k for k, v in config['services'].items()
        if v.get('image', '').startswith('brewblox/brewblox-devcon-spark')
    ]
    zipf = zipfile.ZipFile(file, 'w', zipfile.ZIP_DEFLATED)

    # Always save .env
    utils.info('Exporting .env')
    zipf.write('.env')

    # Always save datastore
    utils.info('Exporting datastore')
    resp = requests.post(store_url + '/mget',
                         json={'namespace': '', 'filter': '*'},
                         verify=False)
    resp.raise_for_status()
    zipf.writestr('global.redis.json', resp.text)

    if save_compose:
        utils.info('Exporting docker-compose.yml')
        zipf.write('docker-compose.yml')

    for spark in sparks:
        utils.info("Exporting Spark blocks from '{}'".format(spark))
        resp = requests.post('{}/{}/blocks/backup/save'.format(utils.host_url(), spark), verify=False)
        try:
            resp.raise_for_status()
            zipf.writestr(spark + '.spark.json', resp.text)
        except Exception as ex:
            if ignore_spark_error:
                utils.info("Skipping Spark '{}' due to error: {}".format(spark, str(ex)))
            else:
                raise ex

    for fname in [*glob('node-red/*.js*'), *glob('node-red/lib/**/*.js*')]:
        zipf.write(fname)

    zipf.close()
    click.echo(path.abspath(file))
    utils.info('Done!')
示例#20
0
def setup(port_check):
    """Run first-time setup in Brewblox directory.

    Run after brewblox-ctl install, in the newly created Brewblox directory.
    This will create all required configuration files for your system.

    You can safely use this command to partially reset your system.
    Before making any changes, it will check for existing files,
    and prompt if any are found. It will do so separately for docker-compose,
    datastore, history, and gateway files.
    Choose to skip any, and the others will still be created and configured.

    \b
    Steps:
        - Check whether files already exist.
        - Set .env values.
        - Create docker-compose configuration files. (Optional)
        - Pull docker images.
        - Create datastore (CouchDB) directory.      (Optional)
        - Create history (InfluxDB) directory.       (Optional)
        - Create gateway (Traefik) directory.        (Optional)
        - Create SSL certificates.                   (Optional)
        - Start and configure services.              (Optional)
        - Stop all services.
        - Set version number in .env.
    """
    utils.check_config()
    utils.confirm_mode()

    sudo = utils.optsudo()
    datastore_url = utils.datastore_url()
    history_url = utils.history_url()
    upped_services = ['traefik', 'influx', 'history']
    preset_modules = ['services', 'dashboards', 'dashboard-items']

    if port_check:
        check_ports()

    skip_compose = \
        utils.path_exists('./docker-compose.yml') \
        and utils.confirm('This directory already contains a docker-compose.yml file. ' +
                          'Do you want to keep it?')

    skip_datastore = \
        utils.path_exists('./couchdb/') \
        and utils.confirm('This directory already contains Couchdb datastore files. ' +
                          'Do you want to keep them?')

    skip_history = \
        utils.path_exists('./influxdb/') \
        and utils.confirm('This directory already contains Influx history files. ' +
                          'Do you want to keep them?')

    skip_gateway = \
        utils.path_exists('./traefik/') \
        and utils.confirm('This directory already contains Traefik gateway files. ' +
                          'Do you want to keep them?')

    utils.info('Setting .env values...')
    for key, default_val in const.ENV_DEFAULTS.items():
        utils.setenv(key, utils.getenv(key, default_val))

    if not skip_compose:
        utils.info('Copying configuration...')
        sh('cp -f {}/* ./'.format(const.CONFIG_DIR))

    # Stop and pull after we're sure we have a compose file
    utils.info('Stopping services...')
    sh('{}docker-compose down --remove-orphans'.format(sudo))
    utils.info('Pulling docker images...')
    sh('{}docker-compose pull'.format(sudo))

    if not skip_datastore:
        utils.info('Creating datastore directory...')
        upped_services.append('datastore')
        sh('sudo rm -rf ./couchdb/; mkdir ./couchdb/')

    if not skip_history:
        utils.info('Creating history directory...')
        sh('sudo rm -rf ./influxdb/; mkdir ./influxdb/')

    if not skip_gateway:
        utils.info('Creating gateway directory...')
        sh('sudo rm -rf ./traefik/; mkdir ./traefik/')

        utils.info('Creating SSL certificate...')
        sh('{}docker run --rm -v "$(pwd)"/traefik/:/certs/ '.format(sudo) +
           'brewblox/omgwtfssl:{}'.format(utils.docker_tag()))
        sh('sudo chmod 644 traefik/brewblox.crt')
        sh('sudo chmod 600 traefik/brewblox.key')

    # Bring images online that we will send configuration
    utils.info('Starting configured services...')
    sh('{}docker-compose up -d --remove-orphans {}'.format(
        sudo, ' '.join(upped_services)))

    if not skip_datastore:
        # Generic datastore setup
        utils.info('Configuring datastore settings...')
        sh('{} http wait {}'.format(const.CLI, datastore_url))
        sh('{} http put {}/_users'.format(const.CLI, datastore_url))
        sh('{} http put {}/_replicator'.format(const.CLI, datastore_url))
        sh('{} http put {}/_global_changes'.format(const.CLI, datastore_url))
        sh('{} http put {}/{}'.format(const.CLI, datastore_url,
                                      const.UI_DATABASE))
        # Load presets
        utils.info('Loading preset data...')
        for mod in preset_modules:
            sh('{} http post {}/{}/_bulk_docs -f {}/{}.json'.format(
                const.CLI, datastore_url, const.UI_DATABASE, const.PRESETS_DIR,
                mod))

    # Always setup history
    utils.info('Configuring history settings...')
    sh('{} http wait {}/ping'.format(const.CLI, history_url))
    sh('{} http post {}/query/configure'.format(const.CLI, history_url))

    # Setup is done - leave system in stable state
    utils.info('Stopping services...')
    sh('{}docker-compose down'.format(utils.optsudo()))

    # Setup is complete and ok - now set CFG version
    utils.setenv(const.CFG_VERSION_KEY, const.CURRENT_VERSION)
    utils.info('All done!')
示例#21
0
def load(archive,
         load_env,
         load_compose,
         load_datastore,
         load_spark,
         load_node_red,
         update):
    """Load and apply Brewblox settings backup.

    This function uses files generated by `brewblox-ctl backup save` as input.
    You can use the --load-XXXX options to partially load the backup.

    This does not attempt to merge data: it will overwrite current docker-compose.yml,
    datastore entries, and Spark blocks.

    Blocks on Spark services not in the backup file will not be affected.

    If dry-run is enabled, it will echo all configuration from the backup archive.

    Steps:
        - Write .env
        - Read .env values
        - Write docker-compose.yml, run `docker-compose up`.
        - Write all datastore files found in backup.
        - Write all Spark blocks found in backup.
        - Write Node-RED config files found in backup.
        - Run brewblox-ctl update
    """
    utils.check_config()
    utils.confirm_mode()
    urllib3.disable_warnings()

    sudo = utils.optsudo()
    host_url = utils.host_url()
    store_url = utils.datastore_url()

    zipf = zipfile.ZipFile(archive, 'r', zipfile.ZIP_DEFLATED)
    available = zipf.namelist()
    redis_file = 'global.redis.json'
    couchdb_files = [v for v in available if v.endswith('.datastore.json')]
    spark_files = [v for v in available if v.endswith('.spark.json')]
    node_red_files = [v for v in available if v.startswith('node-red/')]

    if load_env and '.env' in available:
        utils.info('Loading .env file')
        with NamedTemporaryFile('w') as tmp:
            data = zipf.read('.env').decode()
            utils.info('Writing .env')
            utils.show_data(data)
            tmp.write(data)
            tmp.flush()
            sh('cp -f {} .env'.format(tmp.name))

        utils.info('Reading .env values')
        load_dotenv(path.abspath('.env'))

    if load_compose:
        if 'docker-compose.yml' in available:
            utils.info('Loading docker-compose.yml')
            config = yaml.safe_load(zipf.read('docker-compose.yml'))
            # Older services may still depend on the `datastore` service
            # The `depends_on` config is useless anyway in a brewblox system
            for svc in config['services'].values():
                with suppress(KeyError):
                    del svc['depends_on']
            utils.write_compose(config)
            sh('{} docker-compose up -d --remove-orphans'.format(sudo))
        else:
            utils.info('docker-compose.yml file not found in backup archive')

    if load_datastore:
        if redis_file in available or couchdb_files:
            utils.info('Waiting for the datastore...')
            sh('{} http wait {}/ping'.format(const.CLI, store_url))
            # Wipe UI/Automation, but leave Spark files
            mdelete_cmd = '{} http post {}/mdelete --quiet -d \'{{"namespace":"{}", "filter":"*"}}\''
            sh(mdelete_cmd.format(const.CLI, store_url, 'brewblox-ui-store'))
            sh(mdelete_cmd.format(const.CLI, store_url, 'brewblox-automation'))
        else:
            utils.info('No datastore files found in backup archive')

        if redis_file in available:
            data = json.loads(zipf.read(redis_file).decode())
            utils.info('Loading {} entries from Redis datastore'.format(len(data['values'])))
            mset(data)

        # Backwards compatibility for UI/automation files from CouchDB
        # The IDs here are formatted as {moduleId}__{objId}
        # The module ID becomes part of the Redis namespace
        for db in ['brewblox-ui-store', 'brewblox-automation']:
            fname = '{}.datastore.json'.format(db)
            if fname not in available:
                continue
            docs = json.loads(zipf.read(fname).decode())
            # Drop invalid names (not prefixed with module ID)
            docs[:] = [d for d in docs if len(d['_id'].split('__', 1)) == 2]
            # Add namespace / ID fields
            for d in docs:
                segments = d['_id'].split('__', 1)
                d['namespace'] = '{}:{}'.format(db, segments[0])
                d['id'] = segments[1]
                del d['_id']
            utils.info('Loading {} entries from database `{}`'.format(len(docs), db))
            mset({'values': docs})

        # Backwards compatibility for Spark service files
        # There is no module ID field here
        spark_db = 'spark-service'
        spark_fname = '{}.datastore.json'.format(spark_db)
        if spark_fname in available:
            docs = json.loads(zipf.read(spark_fname).decode())
            for d in docs:
                d['namespace'] = spark_db
                d['id'] = d['_id']
                del d['_id']
            utils.info('Loading {} entries from database `{}`'.format(len(docs), spark_db))
            mset({'values': docs})

    if load_spark:
        sudo = utils.optsudo()

        if not spark_files:
            utils.info('No Spark files found in backup archive')

        for f in spark_files:
            spark = f[:-len('.spark.json')]
            utils.info('Writing blocks to Spark service {}'.format(spark))
            with NamedTemporaryFile('w') as tmp:
                data = json.loads(zipf.read(f).decode())
                utils.show_data(data)
                json.dump(data, tmp)
                tmp.flush()
                sh('{} http post {}/{}/blocks/backup/load -f {}'.format(const.CLI, host_url, spark, tmp.name))
                sh('{} docker-compose restart {}'.format(sudo, spark))

    if load_node_red and node_red_files:
        sudo = ''
        if [getgid(), getuid()] != [1000, 1000]:
            sudo = 'sudo '

        with TemporaryDirectory() as tmpdir:
            zipf.extractall(tmpdir, members=node_red_files)
            sh('mkdir -p ./node-red')
            sh('{}chown 1000:1000 ./node-red/'.format(sudo))
            sh('{}chown -R 1000:1000 {}'.format(sudo, tmpdir))
            sh('{}cp -rfp {}/node-red/* ./node-red/'.format(sudo, tmpdir))

    zipf.close()

    if update:
        utils.info('Updating brewblox...')
        sh('{} update'.format(const.CLI))

    utils.info('Done!')
示例#22
0
def migrate_couchdb():
    urllib3.disable_warnings()
    sudo = utils.optsudo()
    opts = utils.ctx_opts()
    redis_url = utils.datastore_url()
    couch_url = 'http://localhost:5984'

    utils.info('Migrating datastore from CouchDB to Redis...')

    if opts.dry_run:
        utils.info('Dry run. Skipping migration...')
        return

    if not utils.path_exists('./couchdb/'):
        utils.info('couchdb/ dir not found. Skipping migration...')
        return

    utils.info('Starting a temporary CouchDB container on port 5984...')
    sh(f'{sudo}docker rm -f couchdb-migrate', check=False)
    sh(f'{sudo}docker run --rm -d'
       ' --name couchdb-migrate'
       ' -v "$(pwd)/couchdb/:/opt/couchdb/data/"'
       ' -p "5984:5984"'
       ' treehouses/couchdb:2.3.1')
    sh(f'{const.CLI} http wait {couch_url}')
    sh(f'{const.CLI} http wait {redis_url}/ping')

    resp = requests.get(f'{couch_url}/_all_dbs')
    resp.raise_for_status()
    dbs = resp.json()

    for db in ['brewblox-ui-store', 'brewblox-automation']:
        if db in dbs:
            resp = requests.get(f'{couch_url}/{db}/_all_docs',
                                params={'include_docs': True})
            resp.raise_for_status()
            docs = [v['doc'] for v in resp.json()['rows']]
            # Drop invalid names
            docs[:] = [d for d in docs if len(d['_id'].split('__', 1)) == 2]
            for d in docs:
                segments = d['_id'].split('__', 1)
                d['namespace'] = f'{db}:{segments[0]}'
                d['id'] = segments[1]
                del d['_rev']
                del d['_id']
            resp = requests.post(f'{redis_url}/mset',
                                 json={'values': docs},
                                 verify=False)
            resp.raise_for_status()
            utils.info(f'Migrated {len(docs)} entries from {db}')

    if 'spark-service' in dbs:
        resp = requests.get(f'{couch_url}/spark-service/_all_docs',
                            params={'include_docs': True})
        resp.raise_for_status()
        docs = [v['doc'] for v in resp.json()['rows']]
        for d in docs:
            d['namespace'] = 'spark-service'
            d['id'] = d['_id']
            del d['_rev']
            del d['_id']
        resp = requests.post(f'{redis_url}/mset',
                             json={'values': docs},
                             verify=False)
        resp.raise_for_status()
        utils.info(f'Migrated {len(docs)} entries from spark-service')

    sh(f'{sudo}docker stop couchdb-migrate')
    sh('sudo mv couchdb/ couchdb-migrated-' +
       datetime.now().strftime('%Y%m%d'))
示例#23
0
def log(add_compose, add_system, upload):
    """Generate and share log file for bug reports.

    This command generates a comprehensive report on current system state and logs.
    When reporting bugs, a termbin blink to the output is often the first thing asked for.

    For best results, run when the services are still active.
    Service logs are discarded after `brewblox-ctl down`.

    Care is taken to prevent accidental leaks of confidential information.
    Only known variables are read from .env,
    and the `--no-add-compose` flag allows skipping compose configuration.
    The latter is useful if the configuration contains passwords or tokens.

    To review or edit the output, use the `--no-upload` flag.
    The output will include instructions on how to manually upload the file.

    \b
    Steps:
        - Create ./brewblox.log file.
        - Append Brewblox .env variables.
        - Append software version info.
        - Append service logs.
        - Append content of docker-compose.yml (optional).
        - Append content of docker-compose.shared.yml (optional).
        - Append blocks from Spark services.
        - Append system diagnostics.
        - Upload file to termbin.com for shareable link (optional).
    """
    utils.check_config()
    utils.confirm_mode()
    sudo = utils.optsudo()

    # Create log
    utils.info(f"Log file: {path.abspath('./brewblox.log')}")
    create()
    append('date')

    # Add .env values
    utils.info('Writing Brewblox .env values...')
    header('.env')
    for key in ENV_KEYS:
        append(f'echo "{key}={utils.getenv(key)}"')

    # Add version info
    utils.info('Writing software version info...')
    header('Versions')
    append('uname -a')
    append(f'{const.PY} --version')
    append(f'{sudo}docker --version')
    append(f'{sudo}docker-compose --version')

    # Add active containers
    utils.info('Writing active containers...')
    header('Containers')
    append(f'{sudo}docker-compose ps -a')

    # Add service logs
    try:
        config_names = list(utils.read_compose()['services'].keys())
        shared_names = list(utils.read_shared_compose()['services'].keys())
        names = [n for n in config_names if n not in shared_names] + shared_names
        for name in names:
            utils.info(f'Writing {name} service logs...')
            header(f'Service: {name}')
            append(f'{sudo}docker-compose logs --timestamps --no-color --tail 200 {name}')
    except Exception as ex:
        append('echo ' + shlex.quote(type(ex).__name__ + ': ' + str(ex)))

    # Add compose config
    if add_compose:
        utils.info('Writing docker-compose configuration...')
        header('docker-compose.yml')
        append('cat docker-compose.yml')
        header('docker-compose.shared.yml')
        append('cat docker-compose.shared.yml')
    else:
        utils.info('Skipping docker-compose configuration...')

    # Add blocks
    host_url = utils.host_url()
    services = utils.list_services('brewblox/brewblox-devcon-spark')
    for svc in services:
        utils.info(f'Writing {svc} blocks...')
        header(f'Blocks: {svc}')
        append(f'{const.CLI} http post --pretty {host_url}/{svc}/blocks/all/read')

    # Add system diagnostics
    if add_system:
        utils.info('Writing system diagnostics...')
        header('docker info')
        append(f'{sudo}docker info')
        header('disk usage')
        append('df -hl')
        header('/proc/net/dev')
        append('column -t /proc/net/dev')
        header('/var/log/syslog')
        append('sudo tail -n 500 /var/log/syslog')
        header('dmesg')
        append('dmesg -T')
    else:
        utils.info('Skipping system diagnostics...')

    # Upload
    if upload:
        utils.info('Uploading brewblox.log to termbin.com...')
        sh('cat brewblox.log | nc termbin.com 9999')
    else:
        utils.info('Skipping upload. If you want to manually upload the log, run: ' +
                   click.style('cat brewblox.log | nc termbin.com 9999', fg='green'))
示例#24
0
def migrate_influxdb(
    target: str = 'victoria',
    duration: str = '',
    services: List[str] = [],
    offsets: List[Tuple[str, int]] = [],
):
    """Exports InfluxDB history data.

    The exported data is either immediately imported to the new history database,
    or saved to file.
    """
    opts = utils.ctx_opts()
    sudo = utils.optsudo()
    date = datetime.now().strftime('%Y%m%d_%H%M')

    utils.warn('Depending on the amount of data, this may take some hours.')
    utils.warn(
        'You can use your system as normal while the migration is in progress.'
    )
    utils.warn('The migration can safely be stopped and restarted or resumed.')
    utils.warn(
        'For more info, see https://brewblox.netlify.app/dev/migration/influxdb.html'
    )

    if opts.dry_run:
        utils.info('Dry run. Skipping migration...')
        return

    if not utils.path_exists('./influxdb/'):
        utils.info('influxdb/ dir not found. Skipping migration...')
        return

    utils.info('Starting InfluxDB container...')

    # Stop container in case previous migration was cancelled
    sh(f'{sudo}docker stop influxdb-migrate > /dev/null', check=False)

    # Start standalone container
    # We'll communicate using 'docker exec', so no need to publish a port
    sh(f'{sudo}docker run '
       '--rm -d '
       '--name influxdb-migrate '
       '-v "$(pwd)/influxdb:/var/lib/influxdb" '
       'influxdb:1.8 '
       '> /dev/null')

    # Do a health check until startup is done
    inner_cmd = 'curl --output /dev/null --silent --fail http://localhost:8086/health'
    bash_cmd = f'until $({inner_cmd}); do sleep 1 ; done'
    sh(f"{sudo}docker exec influxdb-migrate bash -c '{bash_cmd}'")

    # Determine relevant measurement
    # Export all of them if not specified by user
    if not services:
        services = _influx_measurements()

    utils.info(f'Exporting services: {", ".join(services)}')

    # Export data and import to target
    for svc in services:
        offset = next((v for v in offsets if v[0] == svc), ('default', 0))[1]
        _copy_influx_measurement(svc, date, duration, target, offset)

    # Stop migration container
    sh(f'{sudo}docker stop influxdb-migrate > /dev/null', check=False)
示例#25
0
def update(update_ctl, update_ctl_done, pull, update_system, migrate, prune,
           from_version):
    """Download and apply updates.

    This is the one-stop-shop for updating your Brewblox install.
    You can use any of the options to fine-tune the update by enabling or disabling subroutines.

    By default, all options are enabled.

    --update-ctl/--no-update-ctl: Whether to download and install new versions of
    of brewblox-ctl and brewblox-ctl lib. If this flag is set, update will download the new version
    and then restart itself. This way, the migrate is done with the latest version of brewblox-ctl.

    If you're using dry run mode, you'll notice the hidden option --update-ctl-done.
    You can use it to watch the rest of the update: it\'s a flag to avoid endless loops.

    --pull/--no-pull. Whether to pull docker images.
    This is useful if any of your services is using a local image (not from Docker Hub).

    --update-system/--no-update-system determines whether

    --migrate/--no-migrate. Updates regularly require changes to configuration.
    Required changes are applied here.

    --prune/--no-prune. Updates to docker images can leave unused old versions
    on your system. These can be pruned to free up disk space.
    This includes all images and volumes on your system, and not just those created by Brewblox.

    \b
    Steps:
        - Check whether any system fixes must be applied.
        - Update brewblox-ctl and brewblox-ctl libs.
        - Stop services.
        - Update Avahi config.
        - Update system packages.
        - Migrate configuration files.
        - Pull Docker images.
        - Prune unused Docker images and volumes.
        - Start services.
        - Migrate service configuration.
        - Write version number to .env file.
    """
    utils.check_config()
    utils.confirm_mode()
    sudo = utils.optsudo()

    prev_version = StrictVersion(from_version)
    check_version(prev_version)

    if update_ctl and not update_ctl_done:
        utils.info('Updating brewblox-ctl...')
        utils.pip_install('brewblox-ctl')
        utils.info('Updating brewblox-ctl lib...')
        utils.load_ctl_lib()
        # Restart ctl - we just replaced the source code
        sh(' '.join([const.PY, *const.ARGS, '--update-ctl-done']))
        return

    utils.info('Stopping services...')
    sh(f'{sudo}docker-compose down')

    # Download and install the new brewblox-ctl
    utils.info('Upgrading brewblox-ctl to the unified version...')
    if utils.command_exists('apt') and update_system:  # pragma: no branch
        utils.info('Installing dependencies...')
        sh('sudo apt update && sudo apt install -y python3-venv')

    release = utils.getenv(const.RELEASE_KEY)
    sh(f'wget -q -O ./brewblox-ctl.tar.gz https://brewblox.blob.core.windows.net/ctl/{release}/brewblox-ctl.tar.gz'
       )
    utils.info('Creating virtual env...')
    sh(f'{const.PY} -m venv .venv')
    utils.info('Installing packages...')
    sh(' && '.join([
        '. .venv/bin/activate',
        'python3 -m pip install setuptools wheel pip',
        'python3 -m pip install ./brewblox-ctl.tar.gz',
        ' '.join(['python3 -m brewblox_ctl',
                  *const.ARGS[1:]]),  # Already have the --update-ctl-done arg
    ]))
示例#26
0
def discover_device(discovery_type):
    utils.info('Discovering devices...')
    if discovery_type in ['all', 'usb']:
        yield from discover_usb()
    if discovery_type in ['all', 'wifi']:
        yield from discover_wifi()
示例#27
0
def setup(ctx, avahi_config, pull, port_check):
    """Run first-time setup in Brewblox directory.

    Run after brewblox-ctl install, in the newly created Brewblox directory.
    This will create all required configuration files for your system.

    You can safely use this command to partially reset your system.
    Before making any changes, it will check for existing files,
    and prompt if any are found. It will do so separately for docker-compose,
    datastore, history, and gateway files.
    Choose to skip any, and the others will still be created and configured.

    \b
    Steps:
        - Check whether files already exist.
        - Set .env values.
        - Update avahi-daemon config.                (Optional)
        - Create docker-compose configuration files. (Optional)
        - Pull docker images.                        (Optional)
        - Create datastore (Redis) directory.        (Optional)
        - Create history (Victoria) directory.       (Optional)
        - Create gateway (Traefik) directory.        (Optional)
        - Create SSL certificates.                   (Optional)
        - Start and configure services.              (Optional)
        - Stop all services.
        - Set version number in .env.
    """
    utils.check_config()
    utils.confirm_mode()

    sudo = utils.optsudo()

    if port_check:
        check_ports()

    skip_compose = \
        utils.path_exists('./docker-compose.yml') \
        and utils.confirm('This directory already contains a docker-compose.yml file. ' +
                          'Do you want to keep it?')

    skip_datastore = \
        utils.path_exists('./redis/') \
        and utils.confirm('This directory already contains Redis datastore files. ' +
                          'Do you want to keep them?')

    skip_history = \
        utils.path_exists('./victoria/') \
        and utils.confirm('This directory already contains Victoria history files. ' +
                          'Do you want to keep them?')

    skip_gateway = \
        utils.path_exists('./traefik/') \
        and utils.confirm('This directory already contains Traefik gateway files. ' +
                          'Do you want to keep them?')

    skip_eventbus = \
        utils.path_exists('./mosquitto/') \
        and utils.confirm('This directory already contains Mosquitto config files. ' +
                          'Do you want to keep them?')

    utils.info('Setting .env values...')
    for key, default_val in const.ENV_DEFAULTS.items():
        utils.setenv(key, utils.getenv(key, default_val))

    if avahi_config:
        utils.update_avahi_config()

    utils.info('Copying docker-compose.shared.yml...')
    sh(f'cp -f {const.CONFIG_DIR}/docker-compose.shared.yml ./')

    if not skip_compose:
        utils.info('Copying docker-compose.yml...')
        sh(f'cp -f {const.CONFIG_DIR}/docker-compose.yml ./')

    # Stop and pull after we're sure we have a compose file
    utils.info('Stopping services...')
    sh(f'{sudo}docker-compose down')

    if pull:
        utils.info('Pulling docker images...')
        sh(f'{sudo}docker-compose pull')

    if not skip_datastore:
        utils.info('Creating datastore directory...')
        sh('sudo rm -rf ./redis/; mkdir ./redis/')

    if not skip_history:
        utils.info('Creating history directory...')
        sh('sudo rm -rf ./victoria/; mkdir ./victoria/')

    if not skip_gateway:
        utils.info('Creating gateway directory...')
        sh('sudo rm -rf ./traefik/; mkdir ./traefik/')

        utils.info('Creating SSL certificate...')
        ctx.invoke(makecert)

    if not skip_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/')

    # Setup is complete and ok - now set CFG version
    utils.setenv(const.CFG_VERSION_KEY, const.CURRENT_VERSION)
    utils.info('All done!')