Exemple #1
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)
Exemple #2
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': [
            f'./{name}:/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.'
        )
Exemple #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')
Exemple #4
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)
Exemple #5
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)
Exemple #6
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))
Exemple #7
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/')
Exemple #8
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'])
Exemple #9
0
def expose(ctx, delete, service, value):
    """Add exposed port to docker-compose.yml for backend service"""
    config = utils.read_compose()

    ports = nested_setdefault(config, [('services', {}), (service, {}),
                                       ('ports', [])])

    if (value in ports) ^ delete:
        return  # already in desired state

    if delete:
        ports.remove(value)
    else:
        ports.append(value)

    config['services'] = clean_empty(config['services'])
    utils.write_compose(config)
    restart_services(ctx)
Exemple #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)

        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]))
Exemple #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': [
            f'./{name}:/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')
Exemple #12
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)
Exemple #13
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))
Exemple #14
0
def add_node_red():
    """
    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 name in config['services']:
        click.echo('The {} service already exists'.format(name))
        raise SystemExit(1)

    config['services'][name] = {
        'image': 'brewblox/node-red:${BREWBLOX_RELEASE}',
        'restart': 'unless-stopped',
        'volumes': [
            './{}:/data'.format(name),
        ]
    }

    sh('mkdir -p ./{}'.format(name))
    if [getgid(), getuid()] != [1000, 1000]:
        sh('sudo chown 1000:1000 ./{}'.format(name))

    utils.write_compose(config)
    click.echo("Added Node-RED service '{}'.".format(name))
    if utils.confirm("Do you want to run 'brewblox-ctl up' now?"):
        sh('{}docker-compose up -d --remove-orphans'.format(sudo))
        click.echo(
            'Visit https://{}:{}/{} in your browser to load the editor.'.
            format(host, port, name))
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'))
Exemple #16
0
def add_spark(name, discover_now, device_id, discovery, device_host, command,
              force, release, simulation, discovery_release):
    """
    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()

    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)

    if device_id is None and discover_now and not simulation:
        dev = find_device(discovery, discovery_release, device_host)

        if dev:
            device_id = dev.split(' ')[1]
        elif device_host is None:
            # 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,
        '--mdns-port=${BREWBLOX_PORT_MDNS}',
        '--discovery=' + discovery,
    ]

    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':
        'brewblox/brewblox-devcon-spark:{}'.format(utils.docker_tag(release)),
        'privileged':
        True,
        'restart':
        'unless-stopped',
        'labels': [
            'traefik.port=5000',
            'traefik.frontend.rule=PathPrefix: /{}'.format(name),
        ],
        'command':
        ' '.join(commands)
    }

    if simulation:
        volume_dir = 'simulator__{}'.format(name)
        config['services'][name]['volumes'] = [
            './{}:/app/simulator'.format(volume_dir)
        ]
        sh('mkdir -m 777 -p {}'.format(volume_dir))

    utils.write_compose(config)
    click.echo("Added Spark service '{}'.".format(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('{}docker-compose up -d --remove-orphans'.format(sudo))
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'))
Exemple #18
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 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)

    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(
                "The existing Spark service '{}' does not have any connection settings."
                .format(nm))
            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("To reconfigure '{}', please run:".format(nm))
            utils.warn('')
            utils.warn('    brewblox-ctl add-spark -f --name {}'.format(nm))
            utils.warn('')
            utils.select('Press ENTER to continue or Ctrl-C to exit')

    if device_id is None and discover_now and not simulation:
        dev = find_device(discovery_type, device_host)

        if dev:
            device_id = dev['id']
        elif device_host is None:
            # 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': '{}:{}'.format(image_name, utils.docker_tag(release)),
        'privileged': True,
        'restart': 'unless-stopped',
        'command': ' '.join(commands)
    }

    if simulation:
        volume_dir = 'simulator__{}'.format(name)
        config['services'][name]['volumes'] = [
            './{}:/app/simulator'.format(volume_dir)
        ]
        sh('mkdir -m 777 -p {}'.format(volume_dir))

    utils.write_compose(config)
    click.echo("Added Spark service '{}'.".format(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('{}docker-compose up -d --remove-orphans'.format(sudo))
Exemple #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.

    \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!')
Exemple #20
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!')