Ejemplo n.º 1
0
class PkgMgr(with_metaclass(ABCMeta, object)):
    @abstractmethod
    def is_available(self):
        # This method is supposed to return True/False if the package manager is currently installed/usable
        # It can also 'prep' the required systems in the process of detecting availability
        pass

    @abstractmethod
    def list_installed(self):
        # This method should return a list of installed packages, each list item will be passed to get_package_details
        pass

    @abstractmethod
    def get_package_details(self, package):
        # This takes a 'package' item and returns a dictionary with the package information, name and version are minimal requirements
        pass

    def get_packages(self):
        # Take all of the above and return a dictionary of lists of dictionaries (package = list of installed versions)

        installed_packages = {}
        for package in self.list_installed():
            package_details = self.get_package_details(package)
            if 'source' not in package_details:
                package_details['source'] = self.__class__.__name__.lower()
            name = package_details['name']
            if name not in installed_packages:
                installed_packages[name] = [package_details]
            else:
                installed_packages[name].append(package_details)
        return installed_packages
Ejemplo n.º 2
0
class AssiblePlugin(with_metaclass(ABCMeta, object)):

    # allow extra passthrough parameters
    allow_extras = False

    def __init__(self):
        self._options = {}

    def get_option(self, option, hostvars=None):
        if option not in self._options:
            try:
                option_value = C.config.get_config_value(
                    option,
                    plugin_type=get_plugin_class(self),
                    plugin_name=self._load_name,
                    variables=hostvars)
            except AssibleError as e:
                raise KeyError(to_native(e))
            self.set_option(option, option_value)
        return self._options.get(option)

    def set_option(self, option, value):
        self._options[option] = value

    def set_options(self, task_keys=None, var_options=None, direct=None):
        '''
        Sets the _options attribute with the configuration/keyword information for this plugin

        :arg task_keys: Dict with playbook keywords that affect this option
        :arg var_options: Dict with either 'connection variables'
        :arg direct: Dict with 'direct assignment'
        '''
        self._options = C.config.get_plugin_options(get_plugin_class(self),
                                                    self._load_name,
                                                    keys=task_keys,
                                                    variables=var_options,
                                                    direct=direct)

        # allow extras/wildcards from vars that are not directly consumed in configuration
        # this is needed to support things like winrm that can have extended protocol options we don't directly handle
        if self.allow_extras and var_options and '_extras' in var_options:
            self.set_option('_extras', var_options['_extras'])

    def has_option(self, option):
        if not self._options:
            self.set_options()
        return option in self._options

    def _check_required(self):
        # FIXME: standardize required check based on config
        pass
Ejemplo n.º 3
0
class TerminalBase(with_metaclass(ABCMeta, object)):
    '''
    A base class for implementing cli connections

    .. note:: Unlike most of Assible, nearly all strings in
        :class:`TerminalBase` plugins are byte strings.  This is because of
        how close to the underlying platform these plugins operate.  Remember
        to mark literal strings as byte string (``b"string"``) and to use
        :func:`~assible.module_utils._text.to_bytes` and
        :func:`~assible.module_utils._text.to_text` to avoid unexpected
        problems.
    '''

    #: compiled bytes regular expressions as stdout
    terminal_stdout_re = []

    #: compiled bytes regular expressions as stderr
    terminal_stderr_re = []

    #: compiled bytes regular expressions to remove ANSI codes
    ansi_re = [
        re.compile(br'\x1b\[\?1h\x1b='),  # CSI ? 1 h ESC =
        re.compile(br'\x08.'),            # [Backspace] .
        re.compile(br"\x1b\[m"),          # ANSI reset code
    ]

    #: terminal initial prompt
    terminal_initial_prompt = None

    #: terminal initial answer
    terminal_initial_answer = None

    #: Send newline after prompt match
    terminal_inital_prompt_newline = True

    def __init__(self, connection):
        self._connection = connection

    def _exec_cli_command(self, cmd, check_rc=True):
        '''
        Executes the CLI command on the remote device and returns the output

        :arg cmd: Byte string command to be executed
        '''
        return self._connection.exec_command(cmd)

    def _get_prompt(self):
        """
        Returns the current prompt from the device

        :returns: A byte string of the prompt
        """
        return self._connection.get_prompt()

    def on_open_shell(self):
        """Called after the SSH session is established

        This method is called right after the invoke_shell() is called from
        the Paramiko SSHClient instance.  It provides an opportunity to setup
        terminal parameters such as disbling paging for instance.
        """
        pass

    def on_close_shell(self):
        """Called before the connection is closed

        This method gets called once the connection close has been requested
        but before the connection is actually closed.  It provides an
        opportunity to clean up any terminal resources before the shell is
        actually closed
        """
        pass

    def on_become(self, passwd=None):
        """Called when privilege escalation is requested

        :kwarg passwd: String containing the password

        This method is called when the privilege is requested to be elevated
        in the play context by setting become to True.  It is the responsibility
        of the terminal plugin to actually do the privilege escalation such
        as entering `enable` mode for instance
        """
        pass

    def on_unbecome(self):
        """Called when privilege deescalation is requested

        This method is called when the privilege changed from escalated
        (become=True) to non escalated (become=False).  It is the responsibility
        of this method to actually perform the deauthorization procedure
        """
        pass

    def on_authorize(self, passwd=None):
        """Deprecated method for privilege escalation

        :kwarg passwd: String containing the password
        """
        return self.on_become(passwd)

    def on_deauthorize(self):
        """Deprecated method for privilege deescalation
        """
        return self.on_unbecome()
Ejemplo n.º 4
0
class CLI(with_metaclass(ABCMeta, object)):
    ''' code behind bin/assible* programs '''

    PAGER = 'less'

    # -F (quit-if-one-screen) -R (allow raw ansi control chars)
    # -S (chop long lines) -X (disable termcap init and de-init)
    LESS_OPTS = 'FRSX'
    SKIP_INVENTORY_DEFAULTS = False

    def __init__(self, args, callback=None):
        """
        Base init method for all command line programs
        """

        if not args:
            raise ValueError('A non-empty list for args is required')

        self.args = args
        self.parser = None
        self.callback = callback

        if C.DEVEL_WARNING and __version__.endswith('dev0'):
            display.warning(
                'You are running the development version of Assible. You should only run Assible from "devel" if '
                'you are modifying the Assible engine, or trying out features under development. This is a rapidly '
                'changing source of code and can become unstable at any point.'
            )

    @abstractmethod
    def run(self):
        """Run the assible command

        Subclasses must implement this method.  It does the actual work of
        running an Assible command.
        """
        self.parse()

        display.vv(to_text(opt_help.version(self.parser.prog)))

        if C.CONFIG_FILE:
            display.v(u"Using %s as config file" % to_text(C.CONFIG_FILE))
        else:
            display.v(u"No config file found; using defaults")

        # warn about deprecated config options
        for deprecated in C.config.DEPRECATED:
            name = deprecated[0]
            why = deprecated[1]['why']
            if 'alternatives' in deprecated[1]:
                alt = ', use %s instead' % deprecated[1]['alternatives']
            else:
                alt = ''
            ver = deprecated[1].get('version')
            date = deprecated[1].get('date')
            collection_name = deprecated[1].get('collection_name')
            display.deprecated("%s option, %s %s" % (name, why, alt),
                               version=ver,
                               date=date,
                               collection_name=collection_name)

    @staticmethod
    def split_vault_id(vault_id):
        # return (before_@, after_@)
        # if no @, return whole string as after_
        if '@' not in vault_id:
            return (None, vault_id)

        parts = vault_id.split('@', 1)
        ret = tuple(parts)
        return ret

    @staticmethod
    def build_vault_ids(vault_ids,
                        vault_password_files=None,
                        ask_vault_pass=None,
                        create_new_password=None,
                        auto_prompt=True):
        vault_password_files = vault_password_files or []
        vault_ids = vault_ids or []

        # convert vault_password_files into vault_ids slugs
        for password_file in vault_password_files:
            id_slug = u'%s@%s' % (C.DEFAULT_VAULT_IDENTITY, password_file)

            # note this makes --vault-id higher precedence than --vault-password-file
            # if we want to intertwingle them in order probably need a cli callback to populate vault_ids
            # used by --vault-id and --vault-password-file
            vault_ids.append(id_slug)

        # if an action needs an encrypt password (create_new_password=True) and we dont
        # have other secrets setup, then automatically add a password prompt as well.
        # prompts cant/shouldnt work without a tty, so dont add prompt secrets
        if ask_vault_pass or (not vault_ids and auto_prompt):

            id_slug = u'%s@%s' % (C.DEFAULT_VAULT_IDENTITY,
                                  u'prompt_ask_vault_pass')
            vault_ids.append(id_slug)

        return vault_ids

    # TODO: remove the now unused args
    @staticmethod
    def setup_vault_secrets(loader,
                            vault_ids,
                            vault_password_files=None,
                            ask_vault_pass=None,
                            create_new_password=False,
                            auto_prompt=True):
        # list of tuples
        vault_secrets = []

        # Depending on the vault_id value (including how --ask-vault-pass / --vault-password-file create a vault_id)
        # we need to show different prompts. This is for compat with older Towers that expect a
        # certain vault password prompt format, so 'promp_ask_vault_pass' vault_id gets the old format.
        prompt_formats = {}

        # If there are configured default vault identities, they are considered 'first'
        # so we prepend them to vault_ids (from cli) here

        vault_password_files = vault_password_files or []
        if C.DEFAULT_VAULT_PASSWORD_FILE:
            vault_password_files.append(C.DEFAULT_VAULT_PASSWORD_FILE)

        if create_new_password:
            prompt_formats['prompt'] = [
                'New vault password (%(vault_id)s): ',
                'Confirm new vault password (%(vault_id)s): '
            ]
            # 2.3 format prompts for --ask-vault-pass
            prompt_formats['prompt_ask_vault_pass'] = [
                'New Vault password: '******'Confirm New Vault password: '******'prompt'] = ['Vault password (%(vault_id)s): ']
            # The format when we use just --ask-vault-pass needs to match 'Vault password:\s*?$'
            prompt_formats['prompt_ask_vault_pass'] = ['Vault password: '******'prompt', 'prompt_ask_vault_pass']:

                # --vault-id some_name@prompt_ask_vault_pass --vault-id other_name@prompt_ask_vault_pass will be a little
                # confusing since it will use the old format without the vault id in the prompt
                built_vault_id = vault_id_name or C.DEFAULT_VAULT_IDENTITY

                # choose the prompt based on --vault-id=prompt or --ask-vault-pass. --ask-vault-pass
                # always gets the old format for Tower compatibility.
                # ie, we used --ask-vault-pass, so we need to use the old vault password prompt
                # format since Tower needs to match on that format.
                prompted_vault_secret = PromptVaultSecret(
                    prompt_formats=prompt_formats[vault_id_value],
                    vault_id=built_vault_id)

                # a empty or invalid password from the prompt will warn and continue to the next
                # without erroring globally
                try:
                    prompted_vault_secret.load()
                except AssibleError as exc:
                    display.warning('Error in vault password prompt (%s): %s' %
                                    (vault_id_name, exc))
                    raise

                vault_secrets.append((built_vault_id, prompted_vault_secret))

                # update loader with new secrets incrementally, so we can load a vault password
                # that is encrypted with a vault secret provided earlier
                loader.set_vault_secrets(vault_secrets)
                continue

            # assuming anything else is a password file
            display.vvvvv('Reading vault password file: %s' % vault_id_value)
            # read vault_pass from a file
            file_vault_secret = get_file_vault_secret(filename=vault_id_value,
                                                      vault_id=vault_id_name,
                                                      loader=loader)

            # an invalid password file will error globally
            try:
                file_vault_secret.load()
            except AssibleError as exc:
                display.warning(
                    'Error in vault password file loading (%s): %s' %
                    (vault_id_name, to_text(exc)))
                raise

            if vault_id_name:
                vault_secrets.append((vault_id_name, file_vault_secret))
            else:
                vault_secrets.append(
                    (C.DEFAULT_VAULT_IDENTITY, file_vault_secret))

            # update loader with as-yet-known vault secrets
            loader.set_vault_secrets(vault_secrets)

        return vault_secrets

    @staticmethod
    def ask_passwords():
        ''' prompt for connection and become passwords if needed '''

        op = context.CLIARGS
        sshpass = None
        becomepass = None
        become_prompt = ''

        become_prompt_method = "BECOME" if C.AGNOSTIC_BECOME_PROMPT else op[
            'become_method'].upper()

        try:
            if op['ask_pass']:
                sshpass = getpass.getpass(prompt="SSH password: "******"%s password[defaults to SSH password]: " % become_prompt_method
            else:
                become_prompt = "%s password: "******"The number of processes (--forks) must be >= 1")

        return op

    @abstractmethod
    def init_parser(self, usage="", desc=None, epilog=None):
        """
        Create an options parser for most assible scripts

        Subclasses need to implement this method.  They will usually call the base class's
        init_parser to create a basic version and then add their own options on top of that.

        An implementation will look something like this::

            def init_parser(self):
                super(MyCLI, self).init_parser(usage="My Assible CLI", inventory_opts=True)
                assible.arguments.option_helpers.add_runas_options(self.parser)
                self.parser.add_option('--my-option', dest='my_option', action='store')
        """
        self.parser = opt_help.create_base_parser(
            os.path.basename(self.args[0]),
            usage=usage,
            desc=desc,
            epilog=epilog,
        )

    @abstractmethod
    def post_process_args(self, options):
        """Process the command line args

        Subclasses need to implement this method.  This method validates and transforms the command
        line arguments.  It can be used to check whether conflicting values were given, whether filenames
        exist, etc.

        An implementation will look something like this::

            def post_process_args(self, options):
                options = super(MyCLI, self).post_process_args(options)
                if options.addition and options.subtraction:
                    raise AssibleOptionsError('Only one of --addition and --subtraction can be specified')
                if isinstance(options.listofhosts, string_types):
                    options.listofhosts = string_types.split(',')
                return options
        """

        # process tags
        if hasattr(options, 'tags') and not options.tags:
            # optparse defaults does not do what's expected
            # More specifically, we want `--tags` to be additive. So we cannot
            # simply change C.TAGS_RUN's default to ["all"] because then passing
            # --tags foo would cause us to have ['all', 'foo']
            options.tags = ['all']
        if hasattr(options, 'tags') and options.tags:
            tags = set()
            for tag_set in options.tags:
                for tag in tag_set.split(u','):
                    tags.add(tag.strip())
            options.tags = list(tags)

        # process skip_tags
        if hasattr(options, 'skip_tags') and options.skip_tags:
            skip_tags = set()
            for tag_set in options.skip_tags:
                for tag in tag_set.split(u','):
                    skip_tags.add(tag.strip())
            options.skip_tags = list(skip_tags)

        # process inventory options except for CLIs that require their own processing
        if hasattr(options, 'inventory') and not self.SKIP_INVENTORY_DEFAULTS:

            if options.inventory:

                # should always be list
                if isinstance(options.inventory, string_types):
                    options.inventory = [options.inventory]

                # Ensure full paths when needed
                options.inventory = [
                    unfrackpath(opt, follow=False) if ',' not in opt else opt
                    for opt in options.inventory
                ]
            else:
                options.inventory = C.DEFAULT_HOST_LIST

        # Dup args set on the root parser and sub parsers results in the root parser ignoring the args. e.g. doing
        # 'assible-galaxy -vvv init' has no verbosity set but 'assible-galaxy init -vvv' sets a level of 3. To preserve
        # back compat with pre-argparse changes we manually scan and set verbosity based on the argv values.
        if self.parser.prog in ['assible-galaxy', 'assible-vault'
                                ] and not options.verbosity:
            verbosity_arg = next(
                iter([arg for arg in self.args if arg.startswith('-v')]), None)
            if verbosity_arg:
                display.deprecated(
                    "Setting verbosity before the arg sub command is deprecated, set the verbosity "
                    "after the sub command",
                    "2.13",
                    collection_name='assible.builtin')
                options.verbosity = verbosity_arg.count('v')

        return options

    def parse(self):
        """Parse the command line args

        This method parses the command line arguments.  It uses the parser
        stored in the self.parser attribute and saves the args and options in
        context.CLIARGS.

        Subclasses need to implement two helper methods, init_parser() and post_process_args() which
        are called from this function before and after parsing the arguments.
        """
        self.init_parser()

        if HAS_ARGCOMPLETE:
            argcomplete.autocomplete(self.parser)

        try:
            options = self.parser.parse_args(self.args[1:])
        except SystemExit as e:
            if (e.code != 0):
                self.parser.exit(status=2,
                                 message=" \n%s " % self.parser.format_help())
            raise
        options = self.post_process_args(options)
        context._init_global_context(options)

    @staticmethod
    def version_info(gitinfo=False):
        ''' return full assible version info '''
        if gitinfo:
            # expensive call, user with care
            assible_version_string = opt_help.version()
        else:
            assible_version_string = __version__
        assible_version = assible_version_string.split()[0]
        assible_versions = assible_version.split('.')
        for counter in range(len(assible_versions)):
            if assible_versions[counter] == "":
                assible_versions[counter] = 0
            try:
                assible_versions[counter] = int(assible_versions[counter])
            except Exception:
                pass
        if len(assible_versions) < 3:
            for counter in range(len(assible_versions), 3):
                assible_versions.append(0)
        return {
            'string': assible_version_string.strip(),
            'full': assible_version,
            'major': assible_versions[0],
            'minor': assible_versions[1],
            'revision': assible_versions[2]
        }

    @staticmethod
    def pager(text):
        ''' find reasonable way to display text '''
        # this is a much simpler form of what is in pydoc.py
        if not sys.stdout.isatty():
            display.display(text, screen_only=True)
        elif 'PAGER' in os.environ:
            if sys.platform == 'win32':
                display.display(text, screen_only=True)
            else:
                CLI.pager_pipe(text, os.environ['PAGER'])
        else:
            p = subprocess.Popen('less --version',
                                 shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
            p.communicate()
            if p.returncode == 0:
                CLI.pager_pipe(text, 'less')
            else:
                display.display(text, screen_only=True)

    @staticmethod
    def pager_pipe(text, cmd):
        ''' pipe text through a pager '''
        if 'LESS' not in os.environ:
            os.environ['LESS'] = CLI.LESS_OPTS
        try:
            cmd = subprocess.Popen(cmd,
                                   shell=True,
                                   stdin=subprocess.PIPE,
                                   stdout=sys.stdout)
            cmd.communicate(input=to_bytes(text))
        except IOError:
            pass
        except KeyboardInterrupt:
            pass

    @staticmethod
    def _play_prereqs():
        options = context.CLIARGS

        # all needs loader
        loader = DataLoader()

        basedir = options.get('basedir', False)
        if basedir:
            loader.set_basedir(basedir)
            add_all_plugin_dirs(basedir)
            AssibleCollectionConfig.playbook_paths = basedir
            default_collection = _get_collection_name_from_path(basedir)
            if default_collection:
                display.warning(u'running with default collection {0}'.format(
                    default_collection))
                AssibleCollectionConfig.default_collection = default_collection

        vault_ids = list(options['vault_ids'])
        default_vault_ids = C.DEFAULT_VAULT_IDENTITY_LIST
        vault_ids = default_vault_ids + vault_ids

        vault_secrets = CLI.setup_vault_secrets(
            loader,
            vault_ids=vault_ids,
            vault_password_files=list(options['vault_password_files']),
            ask_vault_pass=options['ask_vault_pass'],
            auto_prompt=False)
        loader.set_vault_secrets(vault_secrets)

        # create the inventory, and filter it based on the subset specified (if any)
        inventory = InventoryManager(loader=loader,
                                     sources=options['inventory'])

        # create the variable manager, which will be shared throughout
        # the code, ensuring a consistent view of global variables
        variable_manager = VariableManager(
            loader=loader,
            inventory=inventory,
            version_info=CLI.version_info(gitinfo=False))

        return loader, inventory, variable_manager

    @staticmethod
    def get_host_list(inventory, subset, pattern='all'):

        no_hosts = False
        if len(inventory.list_hosts()) == 0:
            # Empty inventory
            if C.LOCALHOST_WARNING and pattern not in C.LOCALHOST:
                display.warning(
                    "provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'"
                )
            no_hosts = True

        inventory.subset(subset)

        hosts = inventory.list_hosts(pattern)
        if not hosts and no_hosts is False:
            raise AssibleError(
                "Specified hosts and/or --limit does not match any hosts")

        return hosts
Ejemplo n.º 5
0
class Display(with_metaclass(Singleton, object)):
    def __init__(self, verbosity=0):

        self.columns = None
        self.verbosity = verbosity

        # list of all deprecation messages to prevent duplicate display
        self._deprecations = {}
        self._warns = {}
        self._errors = {}

        self.b_cowsay = None
        self.noncow = C.ASSIBLE_COW_SELECTION

        self.set_cowsay_info()

        if self.b_cowsay:
            try:
                cmd = subprocess.Popen([self.b_cowsay, "-l"],
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
                (out, err) = cmd.communicate()
                self.cows_available = set([to_text(c) for c in out.split()])
                if C.ASSIBLE_COW_WHITELIST and any(C.ASSIBLE_COW_WHITELIST):
                    self.cows_available = set(
                        C.ASSIBLE_COW_WHITELIST).intersection(
                            self.cows_available)
            except Exception:
                # could not execute cowsay for some reason
                self.b_cowsay = False

        self._set_column_width()

    def set_cowsay_info(self):
        if C.ASSIBLE_NOCOWS:
            return

        if C.ASSIBLE_COW_PATH:
            self.b_cowsay = C.ASSIBLE_COW_PATH
        else:
            for b_cow_path in b_COW_PATHS:
                if os.path.exists(b_cow_path):
                    self.b_cowsay = b_cow_path

    def display(self,
                msg,
                color=None,
                stderr=False,
                screen_only=False,
                log_only=False,
                newline=True):
        """ Display a message to the user

        Note: msg *must* be a unicode string to prevent UnicodeError tracebacks.
        """

        nocolor = msg

        if not log_only:

            has_newline = msg.endswith(u'\n')
            if has_newline:
                msg2 = msg[:-1]
            else:
                msg2 = msg

            if color:
                msg2 = stringc(msg2, color)

            if has_newline or newline:
                msg2 = msg2 + u'\n'

            msg2 = to_bytes(msg2,
                            encoding=self._output_encoding(stderr=stderr))
            if sys.version_info >= (3, ):
                # Convert back to text string on python3
                # We first convert to a byte string so that we get rid of
                # characters that are invalid in the user's locale
                msg2 = to_text(msg2,
                               self._output_encoding(stderr=stderr),
                               errors='replace')

            # Note: After Display() class is refactored need to update the log capture
            # code in 'bin/assible-connection' (and other relevant places).
            if not stderr:
                fileobj = sys.stdout
            else:
                fileobj = sys.stderr

            fileobj.write(msg2)

            try:
                fileobj.flush()
            except IOError as e:
                # Ignore EPIPE in case fileobj has been prematurely closed, eg.
                # when piping to "head -n1"
                if e.errno != errno.EPIPE:
                    raise

        if logger and not screen_only:
            # We first convert to a byte string so that we get rid of
            # color and characters that are invalid in the user's locale
            msg2 = to_bytes(nocolor.lstrip(u'\n'))

            if sys.version_info >= (3, ):
                # Convert back to text string on python3
                msg2 = to_text(msg2, self._output_encoding(stderr=stderr))

            lvl = logging.INFO
            if color:
                # set logger level based on color (not great)
                try:
                    lvl = color_to_log_level[color]
                except KeyError:
                    # this should not happen, but JIC
                    raise AssibleAssertionError(
                        'Invalid color supplied to display: %s' % color)
            # actually log
            logger.log(lvl, msg2)

    def v(self, msg, host=None):
        return self.verbose(msg, host=host, caplevel=0)

    def vv(self, msg, host=None):
        return self.verbose(msg, host=host, caplevel=1)

    def vvv(self, msg, host=None):
        return self.verbose(msg, host=host, caplevel=2)

    def vvvv(self, msg, host=None):
        return self.verbose(msg, host=host, caplevel=3)

    def vvvvv(self, msg, host=None):
        return self.verbose(msg, host=host, caplevel=4)

    def vvvvvv(self, msg, host=None):
        return self.verbose(msg, host=host, caplevel=5)

    def debug(self, msg, host=None):
        if C.DEFAULT_DEBUG:
            if host is None:
                self.display("%6d %0.5f: %s" % (os.getpid(), time.time(), msg),
                             color=C.COLOR_DEBUG)
            else:
                self.display("%6d %0.5f [%s]: %s" %
                             (os.getpid(), time.time(), host, msg),
                             color=C.COLOR_DEBUG)

    def verbose(self, msg, host=None, caplevel=2):

        to_stderr = C.VERBOSE_TO_STDERR
        if self.verbosity > caplevel:
            if host is None:
                self.display(msg, color=C.COLOR_VERBOSE, stderr=to_stderr)
            else:
                self.display("<%s> %s" % (host, msg),
                             color=C.COLOR_VERBOSE,
                             stderr=to_stderr)

    def get_deprecation_message(self,
                                msg,
                                version=None,
                                removed=False,
                                date=None,
                                collection_name=None):
        ''' used to print out a deprecation message.'''
        msg = msg.strip()
        if msg and msg[-1] not in ['!', '?', '.']:
            msg += '.'

        if collection_name == 'assible.builtin':
            collection_name = 'assible-base'

        if removed:
            header = '[DEPRECATED]: {0}'.format(msg)
            removal_fragment = 'This feature was removed'
            help_text = 'Please update your playbooks.'
        else:
            header = '[DEPRECATION WARNING]: {0}'.format(msg)
            removal_fragment = 'This feature will be removed'
            # FUTURE: make this a standalone warning so it only shows up once?
            help_text = 'Deprecation warnings can be disabled by setting deprecation_warnings=False in assible.cfg.'

        if collection_name:
            from_fragment = 'from {0}'.format(collection_name)
        else:
            from_fragment = ''

        if date:
            when = 'in a release after {0}.'.format(date)
        elif version:
            when = 'in version {0}.'.format(version)
        else:
            when = 'in a future release.'

        message_text = ' '.join(
            f for f in
            [header, removal_fragment, from_fragment, when, help_text] if f)

        return message_text

    def deprecated(self,
                   msg,
                   version=None,
                   removed=False,
                   date=None,
                   collection_name=None):
        if not removed and not C.DEPRECATION_WARNINGS:
            return

        message_text = self.get_deprecation_message(
            msg,
            version=version,
            removed=removed,
            date=date,
            collection_name=collection_name)

        if removed:
            raise AssibleError(message_text)

        wrapped = textwrap.wrap(message_text,
                                self.columns,
                                drop_whitespace=False)
        message_text = "\n".join(wrapped) + "\n"

        if message_text not in self._deprecations:
            self.display(message_text.strip(),
                         color=C.COLOR_DEPRECATE,
                         stderr=True)
            self._deprecations[message_text] = 1

    def warning(self, msg, formatted=False):

        if not formatted:
            new_msg = "[WARNING]: %s" % msg
            wrapped = textwrap.wrap(new_msg, self.columns)
            new_msg = "\n".join(wrapped) + "\n"
        else:
            new_msg = "\n[WARNING]: \n%s" % msg

        if new_msg not in self._warns:
            self.display(new_msg, color=C.COLOR_WARN, stderr=True)
            self._warns[new_msg] = 1

    def system_warning(self, msg):
        if C.SYSTEM_WARNINGS:
            self.warning(msg)

    def banner(self, msg, color=None, cows=True):
        '''
        Prints a header-looking line with cowsay or stars with length depending on terminal width (3 minimum)
        '''
        msg = to_text(msg)

        if self.b_cowsay and cows:
            try:
                self.banner_cowsay(msg)
                return
            except OSError:
                self.warning(
                    "somebody cleverly deleted cowsay or something during the PB run.  heh."
                )

        msg = msg.strip()
        try:
            star_len = self.columns - get_text_width(msg)
        except EnvironmentError:
            star_len = self.columns - len(msg)
        if star_len <= 3:
            star_len = 3
        stars = u"*" * star_len
        self.display(u"\n%s %s" % (msg, stars), color=color)

    def banner_cowsay(self, msg, color=None):
        if u": [" in msg:
            msg = msg.replace(u"[", u"")
            if msg.endswith(u"]"):
                msg = msg[:-1]
        runcmd = [self.b_cowsay, b"-W", b"60"]
        if self.noncow:
            thecow = self.noncow
            if thecow == 'random':
                thecow = random.choice(list(self.cows_available))
            runcmd.append(b'-f')
            runcmd.append(to_bytes(thecow))
        runcmd.append(to_bytes(msg))
        cmd = subprocess.Popen(runcmd,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
        (out, err) = cmd.communicate()
        self.display(u"%s\n" % to_text(out), color=color)

    def error(self, msg, wrap_text=True):
        if wrap_text:
            new_msg = u"\n[ERROR]: %s" % msg
            wrapped = textwrap.wrap(new_msg, self.columns)
            new_msg = u"\n".join(wrapped) + u"\n"
        else:
            new_msg = u"ERROR! %s" % msg
        if new_msg not in self._errors:
            self.display(new_msg, color=C.COLOR_ERROR, stderr=True)
            self._errors[new_msg] = 1

    @staticmethod
    def prompt(msg, private=False):
        prompt_string = to_bytes(msg, encoding=Display._output_encoding())
        if sys.version_info >= (3, ):
            # Convert back into text on python3.  We do this double conversion
            # to get rid of characters that are illegal in the user's locale
            prompt_string = to_text(prompt_string)

        if private:
            return getpass.getpass(prompt_string)
        else:
            return input(prompt_string)

    def do_var_prompt(self,
                      varname,
                      private=True,
                      prompt=None,
                      encrypt=None,
                      confirm=False,
                      salt_size=None,
                      salt=None,
                      default=None,
                      unsafe=None):

        result = None
        if sys.__stdin__.isatty():

            do_prompt = self.prompt

            if prompt and default is not None:
                msg = "%s [%s]: " % (prompt, default)
            elif prompt:
                msg = "%s: " % prompt
            else:
                msg = 'input for %s: ' % varname

            if confirm:
                while True:
                    result = do_prompt(msg, private)
                    second = do_prompt("confirm " + msg, private)
                    if result == second:
                        break
                    self.display("***** VALUES ENTERED DO NOT MATCH ****")
            else:
                result = do_prompt(msg, private)
        else:
            result = None
            self.warning("Not prompting as we are not in interactive mode")

        # if result is false and default is not None
        if not result and default is not None:
            result = default

        if encrypt:
            # Circular import because encrypt needs a display class
            from assible.utils.encrypt import do_encrypt
            result = do_encrypt(result, encrypt, salt_size, salt)

        # handle utf-8 chars
        result = to_text(result, errors='surrogate_or_strict')

        if unsafe:
            result = wrap_var(result)
        return result

    @staticmethod
    def _output_encoding(stderr=False):
        encoding = locale.getpreferredencoding()
        # https://bugs.python.org/issue6202
        # Python2 hardcodes an obsolete value on Mac.  Use MacOSX defaults
        # instead.
        if encoding in ('mac-roman', ):
            encoding = 'utf-8'
        return encoding

    def _set_column_width(self):
        if os.isatty(1):
            tty_size = unpack(
                'HHHH', fcntl.ioctl(1, TIOCGWINSZ, pack('HHHH', 0, 0, 0,
                                                        0)))[1]
        else:
            tty_size = 0
        self.columns = max(79, tty_size - 1)
Ejemplo n.º 6
0
class YumDnf(with_metaclass(ABCMeta, object)):
    """
    Abstract class that handles the population of instance variables that should
    be identical between both YUM and DNF modules because of the feature parity
    and shared argument spec
    """
    def __init__(self, module):

        self.module = module

        self.allow_downgrade = self.module.params['allow_downgrade']
        self.autoremove = self.module.params['autoremove']
        self.bugfix = self.module.params['bugfix']
        self.conf_file = self.module.params['conf_file']
        self.disable_excludes = self.module.params['disable_excludes']
        self.disable_gpg_check = self.module.params['disable_gpg_check']
        self.disable_plugin = self.module.params['disable_plugin']
        self.disablerepo = self.module.params.get('disablerepo', [])
        self.download_only = self.module.params['download_only']
        self.download_dir = self.module.params['download_dir']
        self.enable_plugin = self.module.params['enable_plugin']
        self.enablerepo = self.module.params.get('enablerepo', [])
        self.exclude = self.module.params['exclude']
        self.installroot = self.module.params['installroot']
        self.install_repoquery = self.module.params['install_repoquery']
        self.install_weak_deps = self.module.params['install_weak_deps']
        self.list = self.module.params['list']
        self.names = [p.strip() for p in self.module.params['name']]
        self.releasever = self.module.params['releasever']
        self.security = self.module.params['security']
        self.skip_broken = self.module.params['skip_broken']
        self.state = self.module.params['state']
        self.update_only = self.module.params['update_only']
        self.update_cache = self.module.params['update_cache']
        self.validate_certs = self.module.params['validate_certs']
        self.lock_timeout = self.module.params['lock_timeout']

        # It's possible someone passed a comma separated string since it used
        # to be a string type, so we should handle that
        self.names = self.listify_comma_sep_strings_in_list(self.names)
        self.disablerepo = self.listify_comma_sep_strings_in_list(
            self.disablerepo)
        self.enablerepo = self.listify_comma_sep_strings_in_list(
            self.enablerepo)
        self.exclude = self.listify_comma_sep_strings_in_list(self.exclude)

        # Fail if someone passed a space separated string
        # https://github.com/assible/assible/issues/46301
        for name in self.names:
            if ' ' in name and not any(spec in name
                                       for spec in ['@', '>', '<', '=']):
                module.fail_json(
                    msg=
                    'It appears that a space separated string of packages was passed in '
                    'as an argument. To operate on several packages, pass a comma separated '
                    'string of packages or a list of packages.')

        # Sanity checking for autoremove
        if self.state is None:
            if self.autoremove:
                self.state = "absent"
            else:
                self.state = "present"

        if self.autoremove and (self.state != "absent"):
            self.module.fail_json(
                msg="Autoremove should be used alone or with state=absent",
                results=[],
            )

        # This should really be redefined by both the yum and dnf module but a
        # default isn't a bad idea
        self.lockfile = '/var/run/yum.pid'

    @abstractmethod
    def is_lockfile_pid_valid(self):
        return

    def _is_lockfile_present(self):
        return (os.path.isfile(self.lockfile)
                or glob.glob(self.lockfile)) and self.is_lockfile_pid_valid()

    def wait_for_lock(self):
        '''Poll until the lock is removed if timeout is a positive number'''

        if not self._is_lockfile_present():
            return

        if self.lock_timeout > 0:
            for iteration in range(0, self.lock_timeout):
                time.sleep(1)
                if not self._is_lockfile_present():
                    return

        self.module.fail_json(msg='{0} lockfile is held by another process'.
                              format(self.pkg_mgr_name))

    def listify_comma_sep_strings_in_list(self, some_list):
        """
        method to accept a list of strings as the parameter, find any strings
        in that list that are comma separated, remove them from the list and add
        their comma separated elements to the original list
        """
        new_list = []
        remove_from_original_list = []
        for element in some_list:
            if ',' in element:
                remove_from_original_list.append(element)
                new_list.extend([e.strip() for e in element.split(',')])

        for element in remove_from_original_list:
            some_list.remove(element)

        some_list.extend(new_list)

        if some_list == [""]:
            return []

        return some_list

    @abstractmethod
    def run(self):
        raise NotImplementedError
Ejemplo n.º 7
0
class AssibleCollectionConfig(with_metaclass(_AssibleCollectionConfig)):
    pass
Ejemplo n.º 8
0
class FieldAttributeBase(with_metaclass(BaseMeta, object)):

    def __init__(self):

        # initialize the data loader and variable manager, which will be provided
        # later when the object is actually loaded
        self._loader = None
        self._variable_manager = None

        # other internal params
        self._validated = False
        self._squashed = False
        self._finalized = False

        # every object gets a random uuid:
        self._uuid = get_unique_id()

        # we create a copy of the attributes here due to the fact that
        # it was initialized as a class param in the meta class, so we
        # need a unique object here (all members contained within are
        # unique already).
        self._attributes = self.__class__._attributes.copy()
        self._attr_defaults = self.__class__._attr_defaults.copy()
        for key, value in self._attr_defaults.items():
            if callable(value):
                self._attr_defaults[key] = value()

        # and init vars, avoid using defaults in field declaration as it lives across plays
        self.vars = dict()

    def dump_me(self, depth=0):
        ''' this is never called from production code, it is here to be used when debugging as a 'complex print' '''
        if depth == 0:
            display.debug("DUMPING OBJECT ------------------------------------------------------")
        display.debug("%s- %s (%s, id=%s)" % (" " * depth, self.__class__.__name__, self, id(self)))
        if hasattr(self, '_parent') and self._parent:
            self._parent.dump_me(depth + 2)
            dep_chain = self._parent.get_dep_chain()
            if dep_chain:
                for dep in dep_chain:
                    dep.dump_me(depth + 2)
        if hasattr(self, '_play') and self._play:
            self._play.dump_me(depth + 2)

    def preprocess_data(self, ds):
        ''' infrequently used method to do some pre-processing of legacy terms '''
        return ds

    def load_data(self, ds, variable_manager=None, loader=None):
        ''' walk the input datastructure and assign any values '''

        if ds is None:
            raise AssibleAssertionError('ds (%s) should not be None but it is.' % ds)

        # cache the datastructure internally
        setattr(self, '_ds', ds)

        # the variable manager class is used to manage and merge variables
        # down to a single dictionary for reference in templating, etc.
        self._variable_manager = variable_manager

        # the data loader class is used to parse data from strings and files
        if loader is not None:
            self._loader = loader
        else:
            self._loader = DataLoader()

        # call the preprocess_data() function to massage the data into
        # something we can more easily parse, and then call the validation
        # function on it to ensure there are no incorrect key values
        ds = self.preprocess_data(ds)
        self._validate_attributes(ds)

        # Walk all attributes in the class. We sort them based on their priority
        # so that certain fields can be loaded before others, if they are dependent.
        for name, attr in sorted(iteritems(self._valid_attrs), key=operator.itemgetter(1)):
            # copy the value over unless a _load_field method is defined
            target_name = name
            if name in self._alias_attrs:
                target_name = self._alias_attrs[name]
            if name in ds:
                method = getattr(self, '_load_%s' % name, None)
                if method:
                    self._attributes[target_name] = method(name, ds[name])
                else:
                    self._attributes[target_name] = ds[name]

        # run early, non-critical validation
        self.validate()

        # return the constructed object
        return self

    def get_ds(self):
        try:
            return getattr(self, '_ds')
        except AttributeError:
            return None

    def get_loader(self):
        return self._loader

    def get_variable_manager(self):
        return self._variable_manager

    def _post_validate_debugger(self, attr, value, templar):
        value = templar.template(value)
        valid_values = frozenset(('always', 'on_failed', 'on_unreachable', 'on_skipped', 'never'))
        if value and isinstance(value, string_types) and value not in valid_values:
            raise AssibleParserError("'%s' is not a valid value for debugger. Must be one of %s" % (value, ', '.join(valid_values)), obj=self.get_ds())
        return value

    def _validate_attributes(self, ds):
        '''
        Ensures that there are no keys in the datastructure which do
        not map to attributes for this object.
        '''

        valid_attrs = frozenset(self._valid_attrs.keys())
        for key in ds:
            if key not in valid_attrs:
                raise AssibleParserError("'%s' is not a valid attribute for a %s" % (key, self.__class__.__name__), obj=ds)

    def validate(self, all_vars=None):
        ''' validation that is done at parse time, not load time '''
        all_vars = {} if all_vars is None else all_vars

        if not self._validated:
            # walk all fields in the object
            for (name, attribute) in iteritems(self._valid_attrs):

                if name in self._alias_attrs:
                    name = self._alias_attrs[name]

                # run validator only if present
                method = getattr(self, '_validate_%s' % name, None)
                if method:
                    method(attribute, name, getattr(self, name))
                else:
                    # and make sure the attribute is of the type it should be
                    value = self._attributes[name]
                    if value is not None:
                        if attribute.isa == 'string' and isinstance(value, (list, dict)):
                            raise AssibleParserError(
                                "The field '%s' is supposed to be a string type,"
                                " however the incoming data structure is a %s" % (name, type(value)), obj=self.get_ds()
                            )

        self._validated = True

    def squash(self):
        '''
        Evaluates all attributes and sets them to the evaluated version,
        so that all future accesses of attributes do not need to evaluate
        parent attributes.
        '''
        if not self._squashed:
            for name in self._valid_attrs.keys():
                self._attributes[name] = getattr(self, name)
            self._squashed = True

    def copy(self):
        '''
        Create a copy of this object and return it.
        '''

        new_me = self.__class__()

        for name in self._valid_attrs.keys():
            if name in self._alias_attrs:
                continue
            new_me._attributes[name] = shallowcopy(self._attributes[name])
            new_me._attr_defaults[name] = shallowcopy(self._attr_defaults[name])

        new_me._loader = self._loader
        new_me._variable_manager = self._variable_manager
        new_me._validated = self._validated
        new_me._finalized = self._finalized
        new_me._uuid = self._uuid

        # if the ds value was set on the object, copy it to the new copy too
        if hasattr(self, '_ds'):
            new_me._ds = self._ds

        return new_me

    def get_validated_value(self, name, attribute, value, templar):
        if attribute.isa == 'string':
            value = to_text(value)
        elif attribute.isa == 'int':
            value = int(value)
        elif attribute.isa == 'float':
            value = float(value)
        elif attribute.isa == 'bool':
            value = boolean(value, strict=True)
        elif attribute.isa == 'percent':
            # special value, which may be an integer or float
            # with an optional '%' at the end
            if isinstance(value, string_types) and '%' in value:
                value = value.replace('%', '')
            value = float(value)
        elif attribute.isa == 'list':
            if value is None:
                value = []
            elif not isinstance(value, list):
                value = [value]
            if attribute.listof is not None:
                for item in value:
                    if not isinstance(item, attribute.listof):
                        raise AssibleParserError("the field '%s' should be a list of %s, "
                                                 "but the item '%s' is a %s" % (name, attribute.listof, item, type(item)), obj=self.get_ds())
                    elif attribute.required and attribute.listof == string_types:
                        if item is None or item.strip() == "":
                            raise AssibleParserError("the field '%s' is required, and cannot have empty values" % (name,), obj=self.get_ds())
        elif attribute.isa == 'set':
            if value is None:
                value = set()
            elif not isinstance(value, (list, set)):
                if isinstance(value, string_types):
                    value = value.split(',')
                else:
                    # Making a list like this handles strings of
                    # text and bytes properly
                    value = [value]
            if not isinstance(value, set):
                value = set(value)
        elif attribute.isa == 'dict':
            if value is None:
                value = dict()
            elif not isinstance(value, dict):
                raise TypeError("%s is not a dictionary" % value)
        elif attribute.isa == 'class':
            if not isinstance(value, attribute.class_type):
                raise TypeError("%s is not a valid %s (got a %s instead)" % (name, attribute.class_type, type(value)))
            value.post_validate(templar=templar)
        return value

    def post_validate(self, templar):
        '''
        we can't tell that everything is of the right type until we have
        all the variables.  Run basic types (from isa) as well as
        any _post_validate_<foo> functions.
        '''

        # save the omit value for later checking
        omit_value = templar.available_variables.get('omit')

        for (name, attribute) in iteritems(self._valid_attrs):

            if attribute.static:
                value = getattr(self, name)

                # we don't template 'vars' but allow template as values for later use
                if name not in ('vars',) and templar.is_template(value):
                    display.warning('"%s" is not templatable, but we found: %s, '
                                    'it will not be templated and will be used "as is".' % (name, value))
                continue

            if getattr(self, name) is None:
                if not attribute.required:
                    continue
                else:
                    raise AssibleParserError("the field '%s' is required but was not set" % name)
            elif not attribute.always_post_validate and self.__class__.__name__ not in ('Task', 'Handler', 'PlayContext'):
                # Intermediate objects like Play() won't have their fields validated by
                # default, as their values are often inherited by other objects and validated
                # later, so we don't want them to fail out early
                continue

            try:
                # Run the post-validator if present. These methods are responsible for
                # using the given templar to template the values, if required.
                method = getattr(self, '_post_validate_%s' % name, None)
                if method:
                    value = method(attribute, getattr(self, name), templar)
                elif attribute.isa == 'class':
                    value = getattr(self, name)
                else:
                    # if the attribute contains a variable, template it now
                    value = templar.template(getattr(self, name))

                # if this evaluated to the omit value, set the value back to
                # the default specified in the FieldAttribute and move on
                if omit_value is not None and value == omit_value:
                    if callable(attribute.default):
                        setattr(self, name, attribute.default())
                    else:
                        setattr(self, name, attribute.default)
                    continue

                # and make sure the attribute is of the type it should be
                if value is not None:
                    value = self.get_validated_value(name, attribute, value, templar)

                # and assign the massaged value back to the attribute field
                setattr(self, name, value)
            except (TypeError, ValueError) as e:
                value = getattr(self, name)
                raise AssibleParserError("the field '%s' has an invalid value (%s), and could not be converted to an %s."
                                         "The error was: %s" % (name, value, attribute.isa, e), obj=self.get_ds(), orig_exc=e)
            except (AssibleUndefinedVariable, UndefinedError) as e:
                if templar._fail_on_undefined_errors and name != 'name':
                    if name == 'args':
                        msg = "The task includes an option with an undefined variable. The error was: %s" % (to_native(e))
                    else:
                        msg = "The field '%s' has an invalid value, which includes an undefined variable. The error was: %s" % (name, to_native(e))
                    raise AssibleParserError(msg, obj=self.get_ds(), orig_exc=e)

        self._finalized = True

    def _load_vars(self, attr, ds):
        '''
        Vars in a play can be specified either as a dictionary directly, or
        as a list of dictionaries. If the later, this method will turn the
        list into a single dictionary.
        '''

        def _validate_variable_keys(ds):
            for key in ds:
                if not isidentifier(key):
                    raise TypeError("'%s' is not a valid variable name" % key)

        try:
            if isinstance(ds, dict):
                _validate_variable_keys(ds)
                return combine_vars(self.vars, ds)
            elif isinstance(ds, list):
                all_vars = self.vars
                for item in ds:
                    if not isinstance(item, dict):
                        raise ValueError
                    _validate_variable_keys(item)
                    all_vars = combine_vars(all_vars, item)
                return all_vars
            elif ds is None:
                return {}
            else:
                raise ValueError
        except ValueError as e:
            raise AssibleParserError("Vars in a %s must be specified as a dictionary, or a list of dictionaries" % self.__class__.__name__,
                                     obj=ds, orig_exc=e)
        except TypeError as e:
            raise AssibleParserError("Invalid variable name in vars specified for %s: %s" % (self.__class__.__name__, e), obj=ds, orig_exc=e)

    def _extend_value(self, value, new_value, prepend=False):
        '''
        Will extend the value given with new_value (and will turn both
        into lists if they are not so already). The values are run through
        a set to remove duplicate values.
        '''

        if not isinstance(value, list):
            value = [value]
        if not isinstance(new_value, list):
            new_value = [new_value]

        # Due to where _extend_value may run for some attributes
        # it is possible to end up with Sentinel in the list of values
        # ensure we strip them
        value = [v for v in value if v is not Sentinel]
        new_value = [v for v in new_value if v is not Sentinel]

        if prepend:
            combined = new_value + value
        else:
            combined = value + new_value

        return [i for i, _ in itertools.groupby(combined) if i is not None]

    def dump_attrs(self):
        '''
        Dumps all attributes to a dictionary
        '''
        attrs = {}
        for (name, attribute) in iteritems(self._valid_attrs):
            attr = getattr(self, name)
            if attribute.isa == 'class' and hasattr(attr, 'serialize'):
                attrs[name] = attr.serialize()
            else:
                attrs[name] = attr
        return attrs

    def from_attrs(self, attrs):
        '''
        Loads attributes from a dictionary
        '''
        for (attr, value) in iteritems(attrs):
            if attr in self._valid_attrs:
                attribute = self._valid_attrs[attr]
                if attribute.isa == 'class' and isinstance(value, dict):
                    obj = attribute.class_type()
                    obj.deserialize(value)
                    setattr(self, attr, obj)
                else:
                    setattr(self, attr, value)

    def serialize(self):
        '''
        Serializes the object derived from the base object into
        a dictionary of values. This only serializes the field
        attributes for the object, so this may need to be overridden
        for any classes which wish to add additional items not stored
        as field attributes.
        '''

        repr = self.dump_attrs()

        # serialize the uuid field
        repr['uuid'] = self._uuid
        repr['finalized'] = self._finalized
        repr['squashed'] = self._squashed

        return repr

    def deserialize(self, data):
        '''
        Given a dictionary of values, load up the field attributes for
        this object. As with serialize(), if there are any non-field
        attribute data members, this method will need to be overridden
        and extended.
        '''

        if not isinstance(data, dict):
            raise AssibleAssertionError('data (%s) should be a dict but is a %s' % (data, type(data)))

        for (name, attribute) in iteritems(self._valid_attrs):
            if name in data:
                setattr(self, name, data[name])
            else:
                if callable(attribute.default):
                    setattr(self, name, attribute.default())
                else:
                    setattr(self, name, attribute.default)

        # restore the UUID field
        setattr(self, '_uuid', data.get('uuid'))
        self._finalized = data.get('finalized', False)
        self._squashed = data.get('squashed', False)