On Thu, Feb 06, 2014 at 05:24:11PM +0100, Santi Raffa wrote:
> This commit adds the private containers to Python and Haskell.
> 
> Signed-off-by: Santi Raffa <[email protected]>
> ---
>  lib/cli.py                     |  12 ++++
>  lib/config.py                  |   5 +-
>  lib/ht.py                      |  19 ++++-
>  lib/serializer.py              | 155 
> +++++++++++++++++++++++++++++++++++++++++
>  src/Ganeti/JSON.hs             |   1 +
>  src/Ganeti/Types.hs            |  39 ++++++++++-
>  test/hs/Test/Ganeti/Objects.hs |   8 ++-
>  test/hs/Test/Ganeti/OpCodes.hs |  10 +++
>  8 files changed, 244 insertions(+), 5 deletions(-)
> 
> diff --git a/lib/cli.py b/lib/cli.py
> index cb28c09..3bdd2e2 100644
> --- a/lib/cli.py
> +++ b/lib/cli.py
> @@ -44,6 +44,7 @@ from ganeti import netutils
>  from ganeti import qlang
>  from ganeti import objects
>  from ganeti import pathutils
> +from ganeti import serializer
>  
>  from ganeti.runtime import (GetClient)
>  
> @@ -679,6 +680,15 @@ def check_key_val(option, opt, value):  # pylint: 
> disable=W0613
>    return _SplitKeyVal(opt, value, True)
>  
>  
> +def check_key_private_val(option, opt, value):  # pylint: disable=W0613
> +  """Custom parser class for private and secret key=val,key=val options.
> +
> +  This will store the parsed values as a dict {key: val}.
> +
> +  """
> +  return serializer.PrivateDict(_SplitKeyVal(opt, value, True))
> +
> +
>  def _SplitListKeyVal(opt, value):
>    retval = {}
>    for elem in value.split("/"):
> @@ -781,6 +791,7 @@ class CliOption(Option):
>      "multilistidentkeyval",
>      "identkeyval",
>      "keyval",
> +    "keyprivateval",
>      "unit",
>      "bool",
>      "list",
> @@ -790,6 +801,7 @@ class CliOption(Option):
>    TYPE_CHECKER["multilistidentkeyval"] = check_multilist_ident_key_val
>    TYPE_CHECKER["identkeyval"] = check_ident_key_val
>    TYPE_CHECKER["keyval"] = check_key_val
> +  TYPE_CHECKER["keyprivateval"] = check_key_private_val
>    TYPE_CHECKER["unit"] = check_unit
>    TYPE_CHECKER["bool"] = check_bool
>    TYPE_CHECKER["list"] = check_list
> diff --git a/lib/config.py b/lib/config.py
> index fcdefc3..4e146c4 100644
> --- a/lib/config.py
> +++ b/lib/config.py
> @@ -2488,7 +2488,10 @@ class ConfigWriter(object):
>      if destination is None:
>        destination = self._cfg_file
>      self._BumpSerialNo()
> -    txt = serializer.Dump(self._config_data.ToDict())
> +    txt = serializer.DumpJson(
> +      self._config_data.ToDict(_with_private=True),
> +      private_encoder=serializer.EncodeWithPrivateFields
> +    )
>  
>      getents = self._getents()
>      try:
> diff --git a/lib/ht.py b/lib/ht.py
> index 500c391..c7688ba 100644
> --- a/lib/ht.py
> +++ b/lib/ht.py
> @@ -29,7 +29,7 @@ from ganeti import compat
>  from ganeti import utils
>  from ganeti import constants
>  from ganeti import objects
> -
> +from ganeti.serializer import Private
>  
>  _PAREN_RE = re.compile("^[a-zA-Z0-9_-]+$")
>  
> @@ -77,6 +77,9 @@ class _DescWrapper(_WrapperBase):
>    def __str__(self):
>      return self._text
>  
> +  def __repr__(self):
> +    return "<%s %r>" % (self._text, self._fn)
> +
>  
>  class _CommentWrapper(_WrapperBase):
>    """Wrapper class for comment.
> @@ -269,6 +272,8 @@ def TTuple(val):
>  def TDict(val):
>    """Checks if the given value is a dictionary.
>  
> +  Note that L{PrivateDict}s subclass dict and pass this check.
> +
>    """
>    return isinstance(val, dict)
>  
> @@ -416,6 +421,18 @@ def TInstanceOf(cls):
>    return desc(lambda val: isinstance(val, cls))
>  
>  
> +def TPrivate(val_type):
> +  """Checks if a given value is an instance of Private.
> +
> +  """
> +  def fn(val):
> +    return isinstance(val, Private) and val_type(val.Get())
> +
> +  desc = WithDesc("Private %s" % Parens(val_type))
> +
> +  return desc(fn)
> +
> +
>  def TListOf(my_type):
>    """Checks if a given value is a list with all elements of the same type.
>  
> diff --git a/lib/serializer.py b/lib/serializer.py
> index 87217f3..1b3be77 100644
> --- a/lib/serializer.py
> +++ b/lib/serializer.py
> @@ -166,3 +166,158 @@ Dump = DumpJson
>  Load = LoadJson
>  DumpSigned = DumpSignedJson
>  LoadSigned = LoadSignedJson
> +
> +
> +class Private(object):
> +  """Wrap a value so it is hard to leak it accidentally.
> +
> +    >>> x = Private("foo")
> +    >>> print "Value: %s" % x
> +    Value: <redacted>
> +    >>> print "Value: {0}".format(x)
> +    Value: <redacted>
> +    >>> x.upper() == "FOO"
> +    True

Indentation should be on the first column, as discussed offline.

> +
> +  """
> +

Unnecessary newline.

> +  def __init__(self, item, descr="redacted"):
> +    if isinstance(item, Private):
> +      raise ValueError("Attempted to nest Private values.")
> +    self._item = item
> +    self._descr = descr
> +
> +  def Get(self):
> +    "Return the wrapped value."
> +    return self._item
> +
> +  def __str__(self):
> +    return "<{._descr}>".format(self)
> +
> +  def __repr__(self):
> +    return "Private(?, descr='{._descr}')".format(self)
> +
> +  # pylint: disable=W0212
> +  # If it doesn't access _item directly, the call will go through __getattr__
> +  # because this class defines __slots__ and "item" is not in it.
> +  # OTOH, if we do add it there, we'd risk shadowing an "item" attribute.
> +  def __eq__(self, other):
> +    if isinstance(other, Private):
> +      return self._item == other._item
> +    else:
> +      return self._item == other
> +
> +  def __hash__(self):
> +    return hash(self._item)
> +
> +  def __format__(self, *_1, **_2):
> +    return self.__str__()
> +
> +  def __getattr__(self, attr):
> +    return Private(getattr(self._item, attr),
> +                   descr="%s.%s" % (self._descr, attr))
> +
> +  def __call__(self, *args, **kwargs):
> +    return Private(self._item(*args, **kwargs),
> +                   descr="%s()" % self._descr)
> +
> +  # pylint: disable=R0201
> +  # While this could get away with being a function, it needs to be a method.
> +  # Required by the copy.deepcopy function used by FillDict.
> +  def __getnewargs__(self):
> +    return tuple()
> +
> +  def __nonzero__(self):
> +    return bool(self._item)
> +
> +  # Get in the way of Pickle by implementing __slots__ but not __getstate__
> +  # ...and get a performance boost, too.
> +  __slots__ = ["_item", "_descr"]
> +
> +
> +class PrivateDict(dict):
> +  """A dictionary that turns its values to private fields.
> +
> +    >>> PrivateDict()
> +    {}
> +    >>> supersekkrit = PrivateDict({"password": "foobar"})
> +    >>> print supersekkrit["password"]
> +    <password>
> +    >>> supersekkrit["password"].Get()
> +    'foobar'
> +    >>> supersekkrit.GetPrivate("password")
> +    'foobar'
> +    >>> supersekkrit["user"] = "eggspam"
> +    >>> supersekkrit.Unprivate()
> +    {'password': 'foobar', 'user': 'eggspam'}
> +

Indentation as discussed offline.

> +  """
> +

Unnecessary newline.

> +  def __init__(self, data=None):
> +    dict.__init__(self)
> +    self.update(data)
> +
> +  def __setitem__(self, item, value):
> +    if not isinstance(value, Private):
> +      if not isinstance(item, dict):
> +        value = Private(value, descr=item)
> +      else:
> +        value = PrivateDict(value)
> +    dict.__setitem__(self, item, value)
> +
> +  # The actual conversion to Private containers is done by __setitem__
> +
> +  # copied straight from cpython/Lib/UserDict.py
> +  # Copyright (c) 2001-2014 Python Software Foundation; All Rights Reserved
> +  def update(self, other=None, **kwargs):
> +    # Make progressively weaker assumptions about "other"
> +    if other is None:
> +      pass
> +    elif hasattr(other, 'iteritems'):  # iteritems saves memory and lookups
> +      for k, v in other.iteritems():
> +        self[k] = v
> +    elif hasattr(other, 'keys'):
> +      for k in other.keys():
> +        self[k] = other[k]
> +    else:
> +      for k, v in other:
> +        self[k] = v
> +    if kwargs:
> +      self.update(kwargs)
> +
> +  def GetPrivate(self, *args):
> +    """Like dict.get, but extracting the value in the process.
> +
> +    Arguments are semantically equivalent to ``dict.get``
> +
> +      >>> PrivateDict({"foo": "bar"}).GetPrivate("foo")
> +      'bar'
> +      >>> PrivateDict({"foo": "bar"}).GetPrivate("baz", "spam")
> +      'spam'
> +

Indentation.

Rest LGTM. No need to resend.

Thanks,
Jose

> +    """
> +    if len(args) == 1:
> +      key, = args
> +      return self[key].Get()
> +    elif len(args) == 2:
> +      key, default = args
> +      if key not in self:
> +        return default
> +      else:
> +        return self[key].Get()
> +    else:
> +      raise TypeError("GetPrivate() takes 2 arguments (%d given)" % 
> len(args))
> +
> +  def Unprivate(self):
> +    """Turn this dict of Private() values to a dict of values.
> +
> +    >>> PrivateDict({"foo": "bar"}).Unprivate()
> +    {'foo': 'bar'}
> +
> +    @rtype: dict
> +
> +    """
> +    returndict = {}
> +    for key in self:
> +      returndict[key] = self[key].Get()
> +    return returndict
> diff --git a/src/Ganeti/JSON.hs b/src/Ganeti/JSON.hs
> index c1daa5a..fe84d8a 100644
> --- a/src/Ganeti/JSON.hs
> +++ b/src/Ganeti/JSON.hs
> @@ -48,6 +48,7 @@ module Ganeti.JSON
>    , toArray
>    , optionalJSField
>    , optFieldsToObj
> +  , readContainer
>    , HasStringRepr(..)
>    , GenericContainer(..)
>    , Container
> diff --git a/src/Ganeti/Types.hs b/src/Ganeti/Types.hs
> index f3bd4af..5b6bcb7 100644
> --- a/src/Ganeti/Types.hs
> +++ b/src/Ganeti/Types.hs
> @@ -159,6 +159,8 @@ module Ganeti.Types
>    , hotplugTargetToRaw
>    , HotplugAction(..)
>    , hotplugActionToRaw
> +  , Private(..)
> +  , showPrivateJSObject
>    ) where
>  
>  import Control.Monad (liftM)
> @@ -689,7 +691,7 @@ instance JSON.JSON JobIdDep where
>  absoluteJobIdDep :: (Monad m) => JobIdDep -> JobId -> m JobIdDep
>  absoluteJobIdDep (JobDepAbsolute jid) _ = return $ JobDepAbsolute jid
>  absoluteJobIdDep (JobDepRelative rjid) jid =
> -  liftM JobDepAbsolute . makeJobId $ fromJobId jid + fromNegative rjid 
> +  liftM JobDepAbsolute . makeJobId $ fromJobId jid + fromNegative rjid
>  
>  -- | Job Dependency type.
>  data JobDependency = JobDependency JobIdDep [FinalizedJobStatus]
> @@ -702,7 +704,7 @@ instance JSON JobDependency where
>  -- | From job dependency and job id compute an absolute job dependency.
>  absoluteJobDependency :: (Monad m) => JobDependency -> JobId -> m 
> JobDependency
>  absoluteJobDependency (JobDependency jdep fstats) jid =
> -  liftM (flip JobDependency fstats) $ absoluteJobIdDep jdep jid 
> +  liftM (flip JobDependency fstats) $ absoluteJobIdDep jdep jid
>  
>  -- | Valid opcode priorities for submit.
>  $(THH.declareIADT "OpSubmitPriority"
> @@ -889,3 +891,36 @@ $(THH.declareLADT ''String "HotplugTarget"
>    , ("HTNic",  "hotnic")
>    ])
>  $(THH.makeJSONInstance ''HotplugTarget)
> +
> +-- * Private type and instances
> +
> +-- | A container for values that should be happy to be manipulated yet
> +-- refuses to be shown unless explicitly requested.
> +newtype Private a = Private { getPrivate :: a }
> +  deriving Eq
> +
> +instance (Show a, JSON.JSON a) => JSON.JSON (Private a) where
> +  readJSON = liftM Private . JSON.readJSON
> +  showJSON (Private x) = JSON.showJSON x
> +
> +-- | "Show" the value of the field.
> +--
> +-- It would be better not to implement this at all.
> +-- Alas, Show OpCode requires Show Private.
> +instance Show a => Show (Private a) where
> +  show _ = "<redacted>"
> +
> +instance THH.PyValue a => THH.PyValue (Private a) where
> +  showValue (Private x) = "Private(" ++ THH.showValue x ++ ")"
> +
> +instance Functor Private where
> +  fmap f (Private x) = Private $ f x
> +
> +instance Monad Private where
> +  (Private x) >>= f = f x
> +  return = Private
> +
> +showPrivateJSObject :: (JSON.JSON a) =>
> +                       [(String, a)] -> JSON.JSObject (Private JSON.JSValue)
> +showPrivateJSObject value = JSON.toJSObject $ map f value
> +  where f (k, v) = (k, Private $ JSON.showJSON v)
> diff --git a/test/hs/Test/Ganeti/Objects.hs b/test/hs/Test/Ganeti/Objects.hs
> index 96ba802..a07a95b 100644
> --- a/test/hs/Test/Ganeti/Objects.hs
> +++ b/test/hs/Test/Ganeti/Objects.hs
> @@ -219,6 +219,12 @@ instance Arbitrary ClusterNicParams where
>  instance Arbitrary OsParams where
>    arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
>  
> +instance Arbitrary Objects.ClusterOsParamsPrivate where
> +  arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
> +
> +instance Arbitrary a => Arbitrary (Private a) where
> +  arbitrary = Private <$> arbitrary
> +
>  instance Arbitrary ClusterOsParams where
>    arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
>  
> @@ -552,7 +558,7 @@ caseIncludeLogicalIdDrbd :: HUnit.Assertion
>  caseIncludeLogicalIdDrbd =
>    let vg_name = "xenvg" :: String
>        lv_name = "1234sdf-qwef-2134-asff-asd2-23145d.data" :: String
> -      d = 
> +      d =
>          Disk
>            (LIDDrbd8 "node1.example.com" "node2.example.com" 2000 1 5 
> "secret")
>            [ Disk (LIDPlain "onevg" "onelv") [] "disk1" 1000 DiskRdWr Nothing
> diff --git a/test/hs/Test/Ganeti/OpCodes.hs b/test/hs/Test/Ganeti/OpCodes.hs
> index 861e72d..4a502f1 100644
> --- a/test/hs/Test/Ganeti/OpCodes.hs
> +++ b/test/hs/Test/Ganeti/OpCodes.hs
> @@ -431,6 +431,16 @@ genMacPrefix = do
>    octets <- vectorOf 3 $ choose (0::Int, 255)
>    mkNonEmpty . intercalate ":" $ map (printf "%02x") octets
>  
> +-- | JSObject of arbitrary data.
> +--
> +-- Since JSValue does not implement Arbitrary, I'll simply generate
> +-- (String, String) objects.
> +arbitraryPrivateJSObj :: Gen (J.JSObject (Private J.JSValue))
> +arbitraryPrivateJSObj =
> +  constructor <$> (fromNonEmpty <$> genNameNE)
> +              <*> (fromNonEmpty <$> genNameNE)
> +    where constructor k v = showPrivateJSObject [(k, v)]
> +
>  -- | Arbitrary instance for MetaOpCode, defined here due to TH ordering.
>  $(genArbitrary ''OpCodes.MetaOpCode)
>  
> -- 
> 1.9.0.rc1.175.g0b1dcb5
> 

-- 
Jose Antonio Lopes
Ganeti Engineering
Google Germany GmbH
Dienerstr. 12, 80331, München

Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg
Geschäftsführer: Graham Law, Christine Elizabeth Flores
Steuernummer: 48/725/00206
Umsatzsteueridentifikationsnummer: DE813741370

Reply via email to