#!/usr/bin/python
#
#    proof - makes sure charms follow conventions
#
#    Copyright (C) 2011  Canonical Ltd.
#    Author: Clint Byrum <clint.byrum@canonical.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import os.path as path
import os,sys
from stat import *
import yaml
import re

try:
    charm_name = sys.argv[1]
except:
    print "usage: proof [ charm_name | path/to/charm ]"
    print ""
    print "Note that if no path is specfied, charms will be searched for in the"
    print "current dir, or the dir specified in the environment var CHARM_HOME"
    sys.exit(1)

if '/' in charm_name:
  charm_path = charm_name
else:
  charm_home = os.getenv('CHARM_HOME','.')
  charm_path = path.join(charm_home,'charms',charm_name)

yaml_path = path.join(charm_path, 'metadata.yaml')
hooks_path = path.join(charm_path, 'hooks')

exit_code = 0

hook_warnings = [{'re' : re.compile("http://169\.254\.169\.254/"), 
                  'msg': "hook accesses EC2 metadata service directly"}]

def check_hook(hook, required=True):
  global hooks_path

  hook_path = path.join(hooks_path,hook)
  try:
    mode = os.stat(hook_path)[ST_MODE]
    if not mode & S_IXUSR:
      warn(hook + " not executable")
    with open(hook_path, 'r') as hook_file:
      count = 0
      for line in hook_file:
        count += 1
        for warning in hook_warnings:
          if warning['re'].search(line):
            warn("(%s:%d) - %s" % (hook,count,warning['msg']))
    return True
  except OSError as e:
    if required:
        err("missing hook "+hook)
    return False

def check_relation_hooks(relations):
  template_interfaces = ('interface-name')
  template_relations = ('relation-name')

  for r in relations:
    if r in template_relations:
      err("template relations should be renamed to fit charm: "+r)

    has_one = False
    has_one = has_one or check_hook(r+'-relation-changed', required=False)
    has_one = has_one or check_hook(r+'-relation-departed', required=False)
    has_one = has_one or check_hook(r+'-relation-joined', required=False)
    has_one = has_one or check_hook(r+'-relation-broken', required=False)

    if not has_one:
        warn("relation "+r+" has no hooks")

    try:
      interface = relations[r]['interface']
      if interface in template_interfaces:
        err("template interface names should be changed: "+interface)
    except KeyError:
      err("relation missing interface")

def warn(msg):
  global exit_code
  print "W: " + msg
  if exit_code < 100:
    exit_code = 100 

def err(msg):
  global exit_code
  print "E: " + msg
  if exit_code < 200:
    exit_code = 200

def crit(msg):
  """ Called when checking cannot continue """
  global exit_code
  err("FATAL: " + msg)
  sys.exit(exit_code)

try:
  yamlfile= open(yaml_path,'r')
  try:
    charm = yaml.load(yamlfile)
  except Exception as e:
    crit('cannot parse ' + yaml_path + ":" + str(e))

  yamlfile.close()

  # summary should be short
  if len(charm['summary']) > 72:
    warn('summary sould be less than 72') 


  # Must have a hooks dir
  if not path.exists(hooks_path):
    err("no hooks directory")

  # Must have a copyright file
  if not path.exists(path.join(charm_path,'copyright')):
    err("no copyright file")

  # All charms should provide at least one thing
  try:
    provides = charm['provides']  
    check_relation_hooks(provides)
  except KeyError:
    warn("all charms should provide at least one thing")

  try:
    requires = charm['requires']
    check_relation_hooks(requires)
  except KeyError:
    pass

  try:
    peers = charm['peers']
    check_relation_hooks(peers)
  except KeyError:
    pass

  if 'revision' in charm:
    warn("Revision should not be stored in metadata.yaml anymore. Move it to the revision file")
    # revision must be an integer
    try:
      x = int(charm['revision'])
      if x < 0:
        raise ValueError
    except (TypeError, ValueError):
      warn("revision should be a positive integer")

  check_hook('install')
  check_hook('start', required=False)
  check_hook('stop', required=False)
except IOError:
  err("could not find metadata file for " + charm_name)
  exit_code = -1

rev_path = os.path.join(charm_path, 'revision')
if not path.exists(rev_path):
  err("revision file in root of charm is required")
else:
  with open(rev_path, 'r') as rev_file:
      content = rev_file.read().rstrip()
      try:
          rev = int(content)
      except ValueError:
          err("revision file contains non-numeric data")

sys.exit(exit_code)
