Source code for pyfarm.agent.entrypoints.parser

# No shebang line, this module is meant to be imported
#
# Copyright 2014 Oliver Palmer
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Parser
======

Module which forms the basis of a custom :mod:`argparse` based
command line parser which handles setting configuration values
automatically.
"""

import os
from argparse import (
    SUPPRESS, ArgumentParser, _StoreAction, _StoreConstAction,
    _SubParsersAction, _StoreTrueAction, _StoreFalseAction, _AppendAction,
    _AppendConstAction)
from errno import EEXIST
from functools import partial, wraps
from os.path import isdir, isfile, abspath
from uuid import UUID

from netaddr import AddrFormatError, IPAddress

from pyfarm.core.enums import OS, NUMERIC_TYPES
from pyfarm.core.utility import convert

from pyfarm.agent.logger import getLogger
from pyfarm.agent.config import config

INFINITE = set(["inf", "infinite", "unlimited"])

logger = getLogger("agent.parser")


[docs]def assert_parser(func): """ ensures that the instance argument passed along to the validation function contains data we expect """ @wraps(func) def run(*args, **kwargs): parser = kwargs.get("parser") assert parser is not None and isinstance(parser, ArgumentParser) return func(*args, **kwargs) return run
[docs]@assert_parser def ip(value, parser=None, flag=None): """make sure the ip address provided is valid""" try: IPAddress(value) except (ValueError, AddrFormatError): parser.error("%s is not a valid ip address for %s" % (value, flag)) else: return value
[docs]@assert_parser def port(value, parser=None, get_uid=None, flag=None): """convert and check to make sure the provided port is valid""" assert callable(get_uid) try: value = convert.ston(value) except (ValueError, SyntaxError): parser.error("%s requires a number" % flag) else: try: low_port = 1 if get_uid() == 0 else 49152 except AttributeError: low_port = 49152 high_port = 65535 if low_port > value or value > high_port: parser.error( "valid port range is %s to %s" % (low_port, high_port)) return value
[docs]@assert_parser def uuid_type(value, parser=None, flag=None): """validates that a string is a valid UUID type""" try: return UUID(value) except ValueError: parser.error("%s cannot be convert to a UUID for %s" % (value, flag))
# Function is not currently tested because uid/gid mapping is system specific, # may require access to external network resources, and internally is # covered for the most part by other tests. # TODO: find a reliable way to test uidgid()
[docs]@assert_parser def uidgid(value=None, flag=None, get_id=None, check_id=None, set_id=None, parser=None): # pragma: no cover """ Retrieves and validates the user or group id for a command line flag """ # make sure the partial function is setting # the input values assert flag is not None assert get_id is not None assert check_id is not None assert set_id is not None if set_id is NotImplemented: logger.info("%s is ignored on %s" % (flag, OS.title())) return elif not value: return # convert the incoming argument to a number or fail try: value = convert.ston(value) except ValueError: parser.error("failed to convert %s to a number" % flag) # make sure the id actually exists try: check_id(value) except KeyError: parser.error( "%s %s does not seem to exist" % (flag, value)) # get the uid/gid of the current process # so we can reset it after checking it original_id = get_id() # Try to set the uid/gid to the value requested, fail # otherwise. We're doing this here because we'll be # able to stop the rest of the code from running faster # and provide a more useful error message. try: set_id(value) except OSError: parser.error( "Failed to set %s to %s, please make sure you have " "the necessary permissions to perform this action. " "Typically you must be running as root." % (flag, value)) # set the uid/gid back to the original value since # the id change should occur inside the form or right # before the agent is started try: set_id(original_id) except OSError: parser.error( "failed to set %s back to the original value" % flag) return value
[docs]@assert_parser def direxists(path, parser=None, flag=None, create=False): """checks to make sure the directory exists""" if create: try: os.makedirs(path) except OSError as e: if e.errno != EEXIST: parser.error( "Failed to create directory %s: %s" % (path, e)) else: logger.debug("Created %s", path) elif not isdir(path): parser.error( "%s, path does not exist or is not " "a directory: %s" % (flag, path)) return abspath(path)
[docs]@assert_parser def fileexists(path, parser=None, flag=None): """checks to make sure the provided file exists""" if not isfile(path): parser.error( "Path %r, which was provided to %s, does not exist" % (path, flag)) return abspath(path)
[docs]@assert_parser def number(value, types=None, parser=None, allow_inf=False, min_=1, flag=None): """convert the given value to a number""" if value == "auto": return value # Internally used if isinstance(value, NUMERIC_TYPES): # pragma: no cover return value elif value.lower() in INFINITE and allow_inf: return float("inf") elif value.lower() in INFINITE and not allow_inf: parser.error("%s does not allow an infinite value" % flag) try: value = convert.ston(value, types=types or NUMERIC_TYPES) if min_ is not None and value < min_: parser.error( "%s's value must be greater than %s" % (flag, min_)) return value except SyntaxError: # could not even parse the string as code parser.error( "%s failed to convert %s to a number" % (flag, repr(value))) except ValueError: # it's a number, but not the right type parser.error( "%s, %s is not an instance of %s" % (flag, repr(value), types))
[docs]@assert_parser def enum(value, parser=None, enum=None, flag=None): """ensures that ``value`` is a valid entry in ``enum``""" assert enum is not None value = value.lower() if value not in enum: parser.error( "invalid enum value %s for %s, valid values are %s" % ( value, flag, list(enum))) return value
integer = partial(number, types=int)
[docs]class ActionMixin(object): """ A mixin which overrides the ``__init__`` and ``__call__`` methods on an action so we can: * Setup attributes to manipulate the config object when the arguments are parsed * Ensure we all required arguments are present * Convert the ``type`` keyword into an internal representation so we don't require as much work when we add arguments to the parser """ # Maps standard Python functions to some more advanced internal # functions. Our internal versions have several additional # options and better error handling. TYPE_MAPPING = { int: integer, isdir: direxists, isfile: fileexists} def __init__(self, *args, **kwargs): self.parser = kwargs.pop("parser") self.config = kwargs.pop("config", False) type_ = kwargs.get("type") type_kwargs = kwargs.pop("type_kwargs", {}) if self.config is not False: if self.config not in config and "default" not in kwargs: raise AssertionError( "Config value `%s` does not exist and no default was " "provided. Please either setup a default in the config " "or provide a default to the argument parser" % self.config) # Update the config with the default if one # was provided if "default" in kwargs: default = kwargs["default"] if callable(kwargs["default"]): default = default() config[self.config] = default if type_ is not None: assert self.parser is not None type_ = self.TYPE_MAPPING.get(type_, type_) partial_kwargs = {"parser": self.parser} partial_kwargs.update(type_kwargs) if "flag" not in partial_kwargs: # Convert one of the option string, preferably the # one starting with --, and use that as the flag # in the type function option_strings = kwargs.get("option_strings", []) assert len(option_strings) >= 1 option = option_strings[0] for option_string in option_strings: if option.startswith("--"): option = option_string break partial_kwargs.update(flag=option) kwargs.update(type=partial(type_, **partial_kwargs)) super(ActionMixin, self).__init__(*args, **kwargs) # Before we run the parser, assert that certain flags are # set on each action object. if self.dest != SUPPRESS: if not self.help: raise AssertionError( "`help` keyword missing for %s" % self.option_strings) if self.config is None: raise AssertionError( "The config keyword is missing for %s. Please provide one " "or set config to False." % self.option_strings) def __call__(self, parser, namespace, values, option_string=None): super(ActionMixin, self).__call__( parser, namespace, values, option_string=option_string) # Set the config value based upon the value which was set # on the resulting action. This will be done when parser_args() # is called. if self.dest != SUPPRESS and self.config not in (False, None): config[self.config] = getattr(namespace, self.dest)
# # Create some classes which mix the class above and the # original action so we can set attributes and work with # the configuration mix_action = lambda class_: type(class_.__name__, (ActionMixin, class_), {}) StoreAction = mix_action(_StoreAction) SubParsersAction = mix_action(_SubParsersAction) StoreConstAction = mix_action(_StoreConstAction) StoreTrueAction = mix_action(_StoreTrueAction) StoreFalseAction = mix_action(_StoreFalseAction) AppendAction = mix_action(_AppendAction) AppendConstAction = mix_action(_AppendConstAction)
[docs]class AgentArgumentParser(ArgumentParser): """ A modified :class:`ArgumentParser` which interfaces with the agent's configuration. """ def __init__(self, *args, **kwargs): super(AgentArgumentParser, self).__init__(*args, **kwargs) # Override the relevant actions with out own so # we have more control of what they're doing self.register("action", None, partial(StoreAction, parser=self)) self.register("action", "store", partial(StoreAction, parser=self)) self.register("action", "store_const", partial(StoreConstAction, parser=self)) self.register("action", "store_true", partial(StoreTrueAction, parser=self)) self.register("action", "store_false", partial(StoreFalseAction, parser=self)) self.register("action", "append", partial(AppendAction, parser=self)) self.register("action", "append_const", partial(AppendConstAction, parser=self)) self.register("action", "parsers", partial(SubParsersAction, parser=self))