Example #1
0
    def build_widget(self):
        self.step_pile = pile = Pile([
            Columns([
                ('fixed', 3, self.icon),
                self.description,
            ],
                    dividechars=1),
            Padding.line_break(""),
            Padding.push_4(self.output),
        ])

        if utils.is_linux() and self.model.needs_sudo:
            pile.contents.append((Padding.line_break(""), pile.options()))
            label = 'This step requires sudo.'
            if not self.app.sudo_pass:
                label += '  Enter sudo password, if needed:'
                self.sudo_input = PasswordEditor()
            columns = [
                ('weight', 0.5, Padding.left(Text(('body', label)), left=5)),
            ]
            if self.sudo_input:
                columns.append(
                    ('weight', 1,
                     Color.string_input(self.sudo_input,
                                        focus_map='string_input focus')))
            pile.contents.append((Columns(columns,
                                          dividechars=3), pile.options()))
Example #2
0
 async def do_steps(self):
     step_metas = common.get_step_metadata_filenames()
     results = OrderedDict()
     for step_meta_path in step_metas:
         step_model = common.load_step(step_meta_path)
         if utils.is_linux():
             if step_model.needs_sudo and not utils.can_sudo():
                 utils.error("Step requires passwordless sudo: {}".format(
                     step_model.title))
                 events.Shutdown.set(1)
                 return
         results[step_model.title] = await common.do_step(step_model,
                                                          utils.info)
     events.PostDeployComplete.set()
     return controllers.use('summary').render(results)
Example #3
0
def get_compatible_clouds(cloud_types=None):
    """ List cloud types compatible with the current spell and controller.

    Arguments:
    clouds: optional initial list of clouds to filter
    Returns:
    List of cloud types
    """
    if cloud_types is None:
        clouds = get_clouds()
        cloud_types = set(c['type'] for c in clouds.values())
        # custom providers don't show up in list-clouds but are valid types
        cloud_types |= set(consts.CUSTOM_PROVIDERS)
    else:
        cloud_types = set(cloud_types)

    _normalize_cloud_types(cloud_types)

    if not is_linux():
        # LXD not available on macOS
        cloud_types -= {'localhost'}

    if app.provider and app.provider.controller:
        # if we already have a controller, we should query
        # it via the API for what clouds it supports; for now,
        # though, just assume it's JAAS and hard-code the options
        cloud_types &= consts.JAAS_CLOUDS

    whitelist = set(app.metadata.cloud_whitelist)
    blacklist = set(app.metadata.cloud_blacklist)

    addons_dir = Path(app.config['spell-dir']) / 'addons'
    for addon in app.selected_addons:
        addon_file = addons_dir / addon / 'metadata.yaml'
        addon_meta = yaml.safe_load(addon_file.read_text())
        whitelist.update(addon_meta.get('cloud-whitelist', []))
        blacklist.update(addon_meta.get('cloud-blacklist', []))

    _normalize_cloud_types(whitelist)
    _normalize_cloud_types(blacklist)

    if len(whitelist) > 0:
        return sorted(cloud_types & whitelist)

    elif len(blacklist) > 0:
        return sorted(cloud_types ^ blacklist)

    return sorted(cloud_types)
Example #4
0
    def generate_additional_input(self):
        """ Generates additional input fields, useful for doing it after
        a previous step is run
        """
        self.set_description(self.model.description, 'body')
        self.icon.set_text(('pending_icon', self.icon.get_text()[0]))
        if utils.is_linux() and self.model.needs_sudo:
            self.step_pile.contents.append(
                (Padding.line_break(""), self.step_pile.options()))
            can_sudo = utils.can_sudo()
            label = 'This step requires sudo.'
            if not can_sudo:
                label += '  Please enter sudo password:'******'weight', 0.5, Padding.left(Text(('body', label)), left=5)),
            ]
            if not can_sudo:
                self.sudo_input = PasswordEditor()
                columns.append(
                    ('weight', 1,
                     Color.string_input(self.sudo_input,
                                        focus_map='string_input focus')))
            self.step_pile.contents.append(
                (Columns(columns, dividechars=3), self.step_pile.options()))

        for i in self.additional_input:
            self.app.log.debug(i)
            self.step_pile.contents.append(
                (Padding.line_break(""), self.step_pile.options()))
            column_input = [('weight', 0.5, Padding.left(i['label'], left=5))]
            if i['input']:
                column_input.append(
                    ('weight', 1,
                     Color.string_input(i['input'],
                                        focus_map='string_input focus')))
            self.step_pile.contents.append(
                (Columns(column_input,
                         dividechars=3), self.step_pile.options()))

        self.button = submit_btn(label="Run", on_press=self.submit)
        self.step_pile.contents.append(
            (Padding.line_break(""), self.step_pile.options()))
        self.step_pile.contents.append((Text(""), self.step_pile.options()))
        self.step_pile.contents.append((HR(), self.step_pile.options()))
        self.show_button()
        self.step_pile.focus_position = self.current_button_index
Example #5
0
    def next_step(self, step_model, step_widget):
        """ handles processing step with input data

        Arguments:
        step_model: step_model returned from widget
        done: if True continues on to the summary view
        """

        if utils.is_linux() and step_model.needs_sudo:
            password = None
            if step_widget.sudo_input:
                password = step_widget.sudo_input.value
            if not utils.can_sudo(password):
                step_widget.set_error(
                    'Sudo failed.  Please check your password and ensure that '
                    'your sudo timeout is not set to zero.')
                step_widget.show_button()
                return

        step_widget.clear_error()
        step_widget.clear_button()
        step_widget.set_icon_state('waiting')

        # Set next button focus here now that the step is complete.
        self.view.steps.popleft()
        next_step = None
        if len(self.view.steps) > 0:
            next_step = self.view.steps[0]
            next_step.generate_additional_input()
            self.view.step_pile.focus_position = self.view.step_pile.focus_position + 1  # noqa

        # merge the step_widget input data into our step model
        for m in step_model.additional_input:
            try:
                matching_widget = next(
                    filter(lambda i: i['key'] == m['key'],
                           step_widget.additional_input))
                m['input'] = matching_widget['input'].value
            except StopIteration:
                app.log.error("Model field has no input: {}".format(m['key']))
                continue
        app.loop.create_task(self._put(step_model, step_widget, next_step))
Example #6
0
    def _build_sudo_field(self):
        if not utils.is_linux() or not self.model.needs_sudo:
            return []

        rows = []
        if not self.app.sudo_pass:
            self.sudo_input = PasswordEditor()
        self.clear_sudo_error()
        columns = [
            ('weight', 0.5, Padding.left(self.sudo_label, left=5)),
        ]
        if self.sudo_input:
            columns.append(
                ('weight', 1,
                 Filler(Color.string_input(self.sudo_input,
                                           focus_map='string_input focus'),
                        valign='bottom')))
        rows.extend([
            Padding.line_break(""),
            Columns(columns, dividechars=3, box_columns=[1]),
        ])
        return rows
Example #7
0
 def render(self):
     for step_meta_path in self.step_metas:
         try:
             model = common.load_step(step_meta_path)
         except common.ValidationError as e:
             app.log.error(e.msg)
             utils.error(e.msg)
             sys.exit(1)
         if utils.is_linux() and model.needs_sudo and not model.can_sudo():
             utils.error("Step requires passwordless sudo: {}".format(
                 model.title))
             sys.exit(1)
         app.log.debug("Running step: {}".format(model))
         try:
             step_model, _ = common.do_step(model,
                                            None,
                                            utils.info)
             self.results[step_model.title] = step_model.result
         except Exception as e:
             utils.error("Failed to run {}: {}".format(model.path, e))
             sys.exit(1)
     self.finish()
Example #8
0
def get_compatible_clouds(cloud_types=None):
    """ List cloud types compatible with the current spell and controller.

    Arguments:
    clouds: optional initial list of clouds to filter
    Returns:
    List of cloud types
    """
    clouds = get_clouds()
    cloud_types = set(cloud_types or (c['type'] for c in clouds.values()))

    if 'lxd' in cloud_types:
        # normalize 'lxd' cloud type to localhost; 'lxd' can happen
        # depending on how the controller was bootstrapped
        cloud_types -= {'lxd'}
        cloud_types |= {'localhost'}

    if not is_linux():
        # LXD not available on macOS
        cloud_types -= {'localhost'}

    if app.current_controller:
        # if we already have a controller, we should query
        # it via the API for what clouds it supports; for now,
        # though, just assume it's JAAS and hard-code the options
        cloud_types &= consts.JAAS_CLOUDS

    whitelist = set(app.config['metadata'].get('cloud-whitelist', []))
    blacklist = set(app.config['metadata'].get('cloud-blacklist', []))
    if len(whitelist) > 0:
        return sorted(cloud_types & whitelist)

    elif len(blacklist) > 0:
        return sorted(cloud_types ^ blacklist)

    return sorted(cloud_types)
Example #9
0
    async def run(self, msg_cb, event_name=None):
        # Define STEP_NAME for use in determining where to store
        # our step results,
        #  state set "conjure-up.$SPELL_NAME.$STEP_NAME.result" "val"
        app.env['CONJURE_UP_STEP'] = self.name

        step_path = Path(app.config['spell-dir']) / 'steps' / self.filename

        if not step_path.is_file():
            return

        step_path = str(step_path)

        msg = "Running step: {}.".format(self.name)
        app.log.info(msg)
        msg_cb(msg)
        if event_name is not None:
            track_event(event_name, "Started", "")

        if not os.access(step_path, os.X_OK):
            raise Exception("Step {} not executable".format(self.title))

        if is_linux() and self.needs_sudo and not await can_sudo():
            raise SudoError('Step "{}" requires sudo: {}'.format(
                self.title,
                'password failed'
                if app.sudo_pass else 'passwordless sudo required',
            ))

        cloud_types = juju.get_cloud_types_by_name()
        provider_type = cloud_types[app.provider.cloud]

        app.env['JUJU_PROVIDERTYPE'] = provider_type
        # not all providers have a credential, e.g., localhost
        app.env['JUJU_CREDENTIAL'] = app.provider.credential or ''
        app.env['JUJU_CONTROLLER'] = app.provider.controller
        app.env['JUJU_MODEL'] = app.provider.model
        app.env['JUJU_REGION'] = app.provider.region or ''
        app.env['CONJURE_UP_SPELLSDIR'] = app.argv.spells_dir

        if provider_type == "maas":
            app.env['MAAS_ENDPOINT'] = app.maas.endpoint
            app.env['MAAS_APIKEY'] = app.maas.api_key

        for step_name, step_data in app.steps_data.items():
            for key, value in step_data.items():
                app.env[key.upper()] = step_data[key]

        for key, value in app.env.items():
            if value is None:
                app.log.warning('Env {} is None; '
                                'replacing with empty string'.format(key))
                app.env[key] = ''

        app.log.debug("Storing environment")
        async with aiofiles.open(step_path + ".env", 'w') as outf:
            for k, v in app.env.items():
                if 'JUJU' in k or 'MAAS' in k or 'CONJURE' in k:
                    await outf.write("{}=\"{}\" ".format(k.upper(), v))

        app.log.debug("Executing script: {}".format(step_path))

        async with aiofiles.open(step_path + ".out", 'w') as outf:
            async with aiofiles.open(step_path + ".err", 'w') as errf:
                proc = await asyncio.create_subprocess_exec(step_path,
                                                            env=app.env,
                                                            stdout=outf,
                                                            stderr=errf)
                async with aiofiles.open(step_path + '.out', 'r') as f:
                    while proc.returncode is None:
                        async for line in f:
                            msg_cb(line)
                        await asyncio.sleep(0.01)

        out_log = Path(step_path + '.out').read_text()
        err_log = Path(step_path + '.err').read_text()

        if proc.returncode != 0:
            app.sentry.context.merge({
                'extra': {
                    'out_log_tail': out_log[-400:],
                    'err_log_tail': err_log[-400:],
                }
            })
            raise Exception("Failure in step {}".format(self.filename))

        # special case for 00_deploy-done to report masked
        # charm hook failures that were retried automatically
        if not app.noreport:
            failed_apps = set()  # only report each charm once
            for line in err_log.splitlines():
                if 'hook failure, will retry' in line:
                    log_leader = line.split()[0]
                    unit_name = log_leader.split(':')[-1]
                    app_name = unit_name.split('/')[0]
                    failed_apps.add(app_name)
            for app_name in failed_apps:
                # report each individually so that Sentry will give us a
                # breakdown of failures per-charm in addition to per-spell
                sentry_report('Retried hook failure',
                              tags={
                                  'app_name': app_name,
                              })

        if event_name is not None:
            track_event(event_name, "Done", "")

        result_key = "conjure-up.{}.{}.result".format(app.config['spell'],
                                                      self.name)
        result = app.state.get(result_key)
        return (result or '')
Example #10
0
    async def run(self, phase, msg_cb, event_name=None):
        # Define STEP_NAME for use in determining where to store
        # our step results,
        #  state set "conjure-up.$SPELL_NAME.$STEP_NAME.result" "val"
        app.env['CONJURE_UP_STEP'] = self.name
        app.env['CONJURE_UP_PHASE'] = phase.value

        step_path = self._build_phase_path(phase)

        if not step_path.is_file():
            return

        if not os.access(str(step_path), os.X_OK):
            app.log.error(
                'Unable to run {} step {} {}, it is not executable'.format(
                    self.source, step_path.stem, phase.value))
            return

        step_path = str(step_path)

        msg = "Running {} step: {} {}.".format(self.source, self.name,
                                               phase.value)
        app.log.info(msg)
        msg_cb(msg)
        if event_name is not None:
            track_event(event_name, "Started", "")

        if is_linux() and self.needs_sudo and not await can_sudo():
            raise SudoError('The "{}" step "{}" requires sudo: {}'.format(
                self.source,
                self.title,
                'password failed'
                if app.sudo_pass else 'passwordless sudo required',
            ))

        app.env['CONJURE_UP_SPELLSDIR'] = app.conjurefile['spells-dir']
        app.env['CONJURE_UP_SESSION_ID'] = app.session_id

        if app.metadata.spell_type == spell_types.JUJU:
            cloud_types = juju.get_cloud_types_by_name()
            provider_type = cloud_types[app.provider.cloud]

            app.env['JUJU_PROVIDERTYPE'] = provider_type
            # not all providers have a credential, e.g., localhost
            app.env['JUJU_CREDENTIAL'] = app.provider.credential or ''
            app.env['JUJU_CONTROLLER'] = app.provider.controller
            app.env['JUJU_MODEL'] = app.provider.model
            app.env['JUJU_REGION'] = app.provider.region or ''

            if provider_type == "maas":
                app.env['MAAS_ENDPOINT'] = app.maas.endpoint
                app.env['MAAS_APIKEY'] = app.maas.api_key

        for step_name, step_data in app.steps_data.items():
            for key, value in step_data.items():
                app.env[key.upper()] = str(step_data[key])

        for key, value in app.env.items():
            if value is None:
                app.log.warning('Env {} is None; '
                                'replacing with empty string'.format(key))
                app.env[key] = ''

        app.log.debug("Storing environment")
        async with aiofiles.open(step_path + ".env", 'w') as outf:
            for k, v in app.env.items():
                if 'JUJU' in k or 'MAAS' in k or 'CONJURE' in k:
                    await outf.write("{}=\"{}\" ".format(k.upper(), v))

        app.log.debug("Executing script: {}".format(step_path))

        out_path = step_path + '.out'
        err_path = step_path + '.err'
        ret, out_log, err_log = await arun([step_path],
                                           stdout=out_path,
                                           stderr=err_path,
                                           cb_stdout=msg_cb)

        if ret != 0:
            app.sentry.context.merge({
                'extra': {
                    'out_log_tail': out_log[-400:],
                    'err_log_tail': err_log[-400:],
                }
            })
            raise Exception("Failure in step {} {}".format(
                self.name, phase.value))

        # special case for 00_deploy-done to report masked
        # charm hook failures that were retried automatically
        if not app.no_report and app.metadata.spell_type == spell_types.JUJU:
            failed_apps = set()  # only report each charm once
            for line in err_log.splitlines():
                if 'hook failure, will retry' in line:
                    log_leader = line.split()[0]
                    unit_name = log_leader.split(':')[-1]
                    app_name = unit_name.split('/')[0]
                    failed_apps.add(app_name)
            for app_name in failed_apps:
                # report each individually so that Sentry will give us a
                # breakdown of failures per-charm in addition to per-spell
                sentry_report('Retried hook failure',
                              tags={
                                  'app_name': app_name,
                              })

        if event_name is not None:
            track_event(event_name, "Done", "")

        return self.get_state('result', phase)