#!/usr/bin/python3

from __future__ import print_function

import subprocess
import shlex
import errno
import sys
import os
import io
import re

from ceph_argparse import * # noqa

keyring_base = '/tmp/cephtest-caps.keyring'

class UnexpectedReturn(Exception):
  def __init__(self, cmd, ret, expected, msg):
    if isinstance(cmd, list):
      self.cmd = ' '.join(cmd)
    else:
      assert isinstance(cmd, str), 'cmd needs to be either a list or a str'
      self.cmd = cmd
    self.cmd = str(self.cmd)
    self.ret = int(ret)
    self.expected = int(expected)
    self.msg = str(msg)

  def __str__(self):
    return repr('{c}: expected return {e}, got {r} ({o})'.format(
        c=self.cmd, e=self.expected, r=self.ret, o=self.msg))

def call(cmd):
  if isinstance(cmd, list):
    args = cmd
  elif isinstance(cmd, str):
    args = shlex.split(cmd)
  else:
    assert False, 'cmd is not a string/unicode nor a list!'

  print('call: {0}'.format(args))
  proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  ret = proc.wait()

  return (ret, proc)

def expect(cmd, expected_ret):

  try:
    (r, p) = call(cmd)
  except ValueError as e:
    print('unable to run {c}: {err}'.format(c=repr(cmd), err=e.message),
          file=sys.stderr)
    return errno.EINVAL

  assert r == p.returncode, \
      'wth? r was supposed to match returncode!'

  if r != expected_ret:
    raise UnexpectedReturn(repr(cmd), r, expected_ret, str(p.stderr.read()))

  return p

def expect_to_file(cmd, expected_ret, out_file):

  # Let the exception be propagated to the caller
  p = expect(cmd, expected_ret)
  assert p.returncode == expected_ret, \
      'expected result doesn\'t match and no exception was thrown!'

  with io.open(out_file, 'ab') as file:
    file.write(p.stdout.read())

  return p

class Command:
  def __init__(self, cid, j):
    self.cid = cid[3:]
    self.perms = j['perm']
    self.module = j['module']

    self.sig = ''
    self.args = []
    for s in j['sig']:
      if not isinstance(s, dict):
        assert isinstance(s, str), \
            'malformatted signature cid {0}: {1}\n{2}'.format(cid,s,j)
        if len(self.sig) > 0:
          self.sig += ' '
        self.sig += s
      else:
        self.args.append(s)

  def __str__(self):
    return repr('command {0}: {1} (requires \'{2}\')'.format(self.cid,\
          self.sig, self.perms))


def destroy_keyring(path):
  if not os.path.exists(path):
    raise Exception('oops! cannot remove inexistent keyring {0}'.format(path))

  # grab all client entities from the keyring
  entities = [m.group(1) for m in [re.match(r'\[client\.(.*)\]', l)
                for l in [str(line.strip())
                  for line in io.open(path,'r')]] if m is not None]

  # clean up and make sure each entity is gone
  for e in entities:
    expect('ceph auth del client.{0}'.format(e), 0)
    expect('ceph auth get client.{0}'.format(e), errno.ENOENT)

  # remove keyring
  os.unlink(path)

  return True

def test_basic_auth():
  # make sure we can successfully add/del entities, change their caps
  # and import/export keyrings.

  expect('ceph auth add client.basicauth', 0)
  expect('ceph auth caps client.basicauth mon \'allow *\'', 0)
  # entity exists and caps do not match
  expect('ceph auth add client.basicauth', errno.EINVAL)
  # this command attempts to change an existing state and will fail
  expect('ceph auth add client.basicauth mon \'allow w\'', errno.EINVAL)
  expect('ceph auth get-or-create client.basicauth', 0)
  expect('ceph auth get-key client.basicauth', 0)
  expect('ceph auth get-or-create client.basicauth2', 0)
  # cleanup
  expect('ceph auth del client.basicauth', 0)
  expect('ceph auth del client.basicauth2', 0)

  return True

def gen_module_keyring(module):
  module_caps = [
      ('all', '{t} \'allow service {s} rwx\'', 0),
      ('none', '', errno.EACCES),
      ('wrong', '{t} \'allow service foobar rwx\'', errno.EACCES),
      ('right', '{t} \'allow service {s} {p}\'', 0),
      ('no-execute', '{t} \'allow service {s} x\'', errno.EACCES)
      ]

  keyring = '{0}.service-{1}'.format(keyring_base,module)
  for perms in 'r rw x'.split():
    for (n,p,r) in module_caps:
      c = p.format(t='mon', s=module, p=perms)
      expect_to_file(
          'ceph auth get-or-create client.{cn}-{cp} {caps}'.format(
          cn=n,cp=perms,caps=c), 0, keyring)

  return keyring


def test_all():


  perms = {
      'good': {
        'broad':[
          ('rwx', 'allow *'),
          ('r', 'allow r'),
          ('rw', 'allow rw'),
          ('x', 'allow x'),
          ],
        'service':[
          ('rwx', 'allow service {s} rwx'),
          ('r', 'allow service {s} r'),
          ('rw', 'allow service {s} rw'),
          ('x', 'allow service {s} x'),
          ],
        'command':[
          ('rwx', 'allow command "{c}"'),
          ],
        'command-with':[
          ('rwx', 'allow command "{c}" with {kv}')
          ],
        'command-with-prefix':[
          ('rwx', 'allow command "{c}" with {key} prefix {val}')
          ]
        },
      'bad': {
        'broad':[
          ('none', ''),
          ],
        'service':[
          ('none1', 'allow service foo rwx'),
          ('none2', 'allow service foo r'),
          ('none3', 'allow service foo rw'),
          ('none4', 'allow service foo x'),
          ],
        'command':[
          ('none', 'allow command foo'),
          ],
        'command-with':[
          ('none', 'allow command "{c}" with foo=bar'),
          ],
        'command-with-prefix':[
          ('none', 'allow command "{c}" with foo prefix bar'),
          ],
        }
      }

  cmds = {
      '':[
        {
          'cmd':('status', '', 'r')
          },
        {
          'pre':'heap start_profiler',
          'cmd':('heap', 'heapcmd=stats', 'rw'),
          'post':'heap stop_profiler'
          }
        ],
      'auth':[
        {
          'pre':'',
          'cmd':('auth ls', '', 'r'),
          'post':''
          },
        {
          'pre':'auth get-or-create client.foo mon \'allow *\'',
          'cmd':('auth caps', 'entity="client.foo"', 'rw'),
          'post':'auth del client.foo'
          }
        ],
      'pg':[
        {
          'cmd':('pg getmap', '', 'r'),
          },
        ],
      'mds':[
        {
          'cmd':('mds getmap', '', 'r'),
          },
        ],
      'mon':[
        {
          'cmd':('mon getmap', '', 'r')
          },
        {
          'cmd':('mon remove', 'name=a', 'rw')
          }
        ],
      'osd':[
        {
          'cmd':('osd getmap', '', 'r'),
          },
        {
          'cmd':('osd pause', '', 'rw'),
          'post':'osd unpause'
          },
        {
          'cmd':('osd crush dump', '', 'r')
          },
        ],
      'config-key':[
          {
            'pre':'config-key set foo bar',
            'cmd':('config-key get', 'key=foo', 'r')
            },
          {
            'pre':'config-key set foo bar',
            'cmd':('config-key del', 'key=foo', 'rw')
            }
          ]
      }

  for (module,cmd_lst) in cmds.items():
    k = keyring_base + '.' + module
    for cmd in cmd_lst:

      (cmd_cmd, cmd_args, cmd_perm) = cmd['cmd']
      cmd_args_key = ''
      cmd_args_val = ''
      if len(cmd_args) > 0:
        (cmd_args_key, cmd_args_val) = cmd_args.split('=')

      print('generating keyring for {m}/{c}'.format(m=module,c=cmd_cmd))
      # gen keyring
      for (good_or_bad,kind_map) in perms.items():
        for (kind,lst) in kind_map.items():
          for (perm, cap) in lst:
            cap_formatted = cap.format(
                s=module,
                c=cmd_cmd,
                kv=cmd_args,
                key=cmd_args_key,
                val=cmd_args_val)

            if len(cap_formatted) == 0:
              run_cap = ''
            else:
              run_cap = 'mon \'{fc}\''.format(fc=cap_formatted)

            cname = 'client.{gb}-{kind}-{p}'.format(
                gb=good_or_bad,kind=kind,p=perm)
            expect_to_file(
                'ceph auth get-or-create {n} {c}'.format(
                  n=cname,c=run_cap), 0, k)
      # keyring generated
      print('testing {m}/{c}'.format(m=module,c=cmd_cmd))

      # test
      for good_bad in perms.keys():
        for (kind,lst) in perms[good_bad].items():
          for (perm,_) in lst:
            cname = 'client.{gb}-{k}-{p}'.format(gb=good_bad,k=kind,p=perm)

          if good_bad == 'good':
            expect_ret = 0
          else:
            expect_ret = errno.EACCES

          if ( cmd_perm not in perm ):
            expect_ret = errno.EACCES
          if 'with' in kind and len(cmd_args) == 0:
            expect_ret = errno.EACCES
          if 'service' in kind and len(module) == 0:
            expect_ret = errno.EACCES

          if 'pre' in cmd and len(cmd['pre']) > 0:
            expect('ceph {0}'.format(cmd['pre']), 0)
          expect('ceph -n {cn} -k {k} {c} {arg_val}'.format(
            cn=cname,k=k,c=cmd_cmd,arg_val=cmd_args_val), expect_ret)
          if 'post' in cmd and len(cmd['post']) > 0:
            expect('ceph {0}'.format(cmd['post']), 0)
      # finish testing
      destroy_keyring(k)


  return True


def test_misc():

  k = keyring_base + '.misc'
  expect_to_file(
      'ceph auth get-or-create client.caps mon \'allow command "auth caps"' \
          ' with entity="client.caps"\'', 0, k)
  expect('ceph -n client.caps -k {kf} quorum_status'.format(kf=k), errno.EACCES)
  expect('ceph -n client.caps -k {kf} auth caps client.caps mon \'allow *\''.format(kf=k), 0)
  expect('ceph -n client.caps -k {kf} quorum_status'.format(kf=k), 0)
  destroy_keyring(k)

def main():

  test_basic_auth()
  test_all()
  test_misc()

  print('OK')

  return 0

if __name__ == '__main__':
  main()
