charmtools-1.0.0/0000775000175000017500000000000012220131557014107 5ustar marcomarco00000000000000charmtools-1.0.0/ez_setup.py0000664000175000017500000002453212214343031016320 0ustar marcomarco00000000000000#!python # NOTE TO LAUNCHPAD DEVELOPERS: This is a bootstrapping file from the # setuptools project. It is imported by our setup.py. """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c11" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" \ % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', } import os import sys try: from hashlib import md5 except ImportError: from md5 import md5 def _validate_md5(egg_name, data): if egg_name in md5_data: digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules \ or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>=" + version) return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay=15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2 import shutil egg_name = "setuptools-%s-py%s.egg" % (version, sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ) from time import sleep sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto, "wb") dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0, egg) from setuptools.command.easy_install import main return main(list(argv) + [egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>=" + version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv) + [download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version", version, "or greater has been " \ + "installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re for name in filenames: base = os.path.basename(name) f = open(name, 'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb') src = f.read() f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile, 'w') f.write(src) f.close() if __name__ == '__main__': if len(sys.argv) > 2 and sys.argv[1] == '--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) charmtools-1.0.0/charmtools.egg-info/0000775000175000017500000000000012220131557017754 5ustar marcomarco00000000000000charmtools-1.0.0/charmtools.egg-info/entry_points.txt0000664000175000017500000000105612220131557023254 0ustar marcomarco00000000000000[console_scripts] charm-getall = charmtools.getall:main charm-unpromulgate = charmtools.unpromulgate:main charm-proof = charmtools.proof:main charm-list = charmtools.list:main charm-review = charmtools.review:main charm-get = charmtools.get:main charm-subscribers = charmtools.subscribers:main charm-review-queue = charmtools.review_queue:main charm = charmtools:main charm-create = charmtools.create:main charm-search = charmtools.search:main juju-charm = charmtools:main charm-update = charmtools.update:main charm-promulgate = charmtools.promulgate:main charmtools-1.0.0/charmtools.egg-info/PKG-INFO0000664000175000017500000000056212220131557021054 0ustar marcomarco00000000000000Metadata-Version: 1.1 Name: charmtools Version: 1.0.0 Summary: Tools for maintaining Juju charms Home-page: https://launchpad.net/charm-tools Author: Marco Ceppi Author-email: UNKNOWN License: GPL v3 Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python charmtools-1.0.0/charmtools.egg-info/requires.txt0000664000175000017500000000007212220131557022353 0ustar marcomarco00000000000000launchpadlib argparse cheetah pyyaml pycrypto paramiko bzrcharmtools-1.0.0/charmtools.egg-info/top_level.txt0000664000175000017500000000001312220131557022500 0ustar marcomarco00000000000000charmtools charmtools-1.0.0/charmtools.egg-info/dependency_links.txt0000664000175000017500000000000112220131557024022 0ustar marcomarco00000000000000 charmtools-1.0.0/charmtools.egg-info/SOURCES.txt0000664000175000017500000000230712220131557021642 0ustar marcomarco00000000000000MANIFEST.in ez_setup.py setup.py charmtools/__init__.py charmtools/charms.py charmtools/create.py charmtools/get.py charmtools/getall.py charmtools/list.py charmtools/mr.py charmtools/promulgate.py charmtools/proof.py charmtools/review.py charmtools/review_queue.py charmtools/search.py charmtools/subscribers.py charmtools/unpromulgate.py charmtools/update.py charmtools.egg-info/PKG-INFO charmtools.egg-info/SOURCES.txt charmtools.egg-info/dependency_links.txt charmtools.egg-info/entry_points.txt charmtools.egg-info/requires.txt charmtools.egg-info/top_level.txt charmtools/templates/charm/README.ex charmtools/templates/charm/config.yaml charmtools/templates/charm/icon.svg charmtools/templates/charm/metadata.yaml charmtools/templates/charm/revision charmtools/templates/charm/hooks/config-changed charmtools/templates/charm/hooks/install charmtools/templates/charm/hooks/relation-name-relation-broken charmtools/templates/charm/hooks/relation-name-relation-changed charmtools/templates/charm/hooks/relation-name-relation-departed charmtools/templates/charm/hooks/relation-name-relation-joined charmtools/templates/charm/hooks/start charmtools/templates/charm/hooks/stop charmtools/templates/charm/hooks/upgrade-charmcharmtools-1.0.0/setup.cfg0000664000175000017500000000007312220131557015730 0ustar marcomarco00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 charmtools-1.0.0/PKG-INFO0000664000175000017500000000056212220131557015207 0ustar marcomarco00000000000000Metadata-Version: 1.1 Name: charmtools Version: 1.0.0 Summary: Tools for maintaining Juju charms Home-page: https://launchpad.net/charm-tools Author: Marco Ceppi Author-email: UNKNOWN License: GPL v3 Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python charmtools-1.0.0/setup.py0000775000175000017500000000323212214640127015625 0ustar marcomarco00000000000000#!/usr/bin/env python # # Copyright 2012 Canonical Ltd. This software is licensed under the # GNU General Public License version 3 (see the file LICENSE). import sys import ez_setup ez_setup.use_setuptools() from setuptools import setup, find_packages __version__ = '1.0.0' setup( name='charmtools', version=__version__, packages=['charmtools'], install_requires=['launchpadlib', 'argparse', 'cheetah', 'pyyaml', 'pycrypto', 'paramiko', 'bzr'], package_data={'charmtools': ['templates/*/*.*', 'templates/*/hooks/*']}, maintainer='Marco Ceppi', description=('Tools for maintaining Juju charms'), license='GPL v3', url='https://launchpad.net/charm-tools', classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python", ], entry_points={ 'console_scripts': [ 'charm = charmtools:main', 'juju-charm = charmtools:main', 'charm-get = charmtools.get:main', 'charm-getall = charmtools.getall:main', 'charm-proof = charmtools.proof:main', 'charm-create = charmtools.create:main', 'charm-list = charmtools.list:main', 'charm-promulgate = charmtools.promulgate:main', 'charm-review = charmtools.review:main', 'charm-review-queue = charmtools.review_queue:main', 'charm-search = charmtools.search:main', 'charm-subscribers = charmtools.subscribers:main', 'charm-unpromulgate = charmtools.unpromulgate:main', 'charm-update = charmtools.update:main', ], }, ) charmtools-1.0.0/charmtools/0000775000175000017500000000000012220131557016262 5ustar marcomarco00000000000000charmtools-1.0.0/charmtools/unpromulgate.py0000664000175000017500000000176212214343031021357 0ustar marcomarco00000000000000#!/usr/bin/env python # Copyright (C) 2013 Marco Ceppi . # # 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 . import os import sys import subprocess def main(): exit_code = subprocess.call([sys.executable, os.path.join(os.path.dirname( os.path.realpath(__file__)), 'promulgate'), '--unpromulgate'] + sys.argv[1:]) sys.exit(exit_code) charmtools-1.0.0/charmtools/create.py0000775000175000017500000001113212220131351020070 0ustar marcomarco00000000000000#!/usr/bin/python # # create - generate Juju charm from template # # Copyright (C) 2011 Canonical Ltd. # Author: Clint Byrum # # 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 . import os import sys import os.path as path import time import shutil import tempfile import textwrap import socket import argparse from Cheetah.Template import Template from stat import ST_MODE def portable_get_maintainer(): """ Portable best effort to determine a maintainer """ if 'NAME' in os.environ: name = os.environ['NAME'] else: try: import pwd name = pwd.getpwuid(os.getuid()).pw_gecos.split(',')[0].strip() if not len(name): name = pwd.getpwuid(os.getuid())[0] except: name = 'Your Name' if not len(name): name = 'Your Name' email = os.environ.get('EMAIL', '%s@%s' % (name.replace(' ', '.'), socket.getfqdn())) return name, email def setup_parser(): parser = argparse.ArgumentParser() parser.add_argument('charmname', help='Name of charm to create.') parser.add_argument('charmhome', nargs='?', help='Dir to create charm in. Defaults to CHARM_HOME ' 'env var or PWD') return parser def apt_fill(package): v = {} try: import apt c = apt.Cache() c.open() p = c[package] print "Found " + package + " package in apt cache, as a result " \ + "charm contents have been pre-populated based on package metadata." v['summary'] = p.summary v['description'] = textwrap.fill(p.description, width=72, subsequent_indent=' ') except: print "Failed to find " + package + " in apt cache, creating " \ + "an empty charm instead." v['summary'] = '' v['description'] = '' return v def main(): parser = setup_parser() args = parser.parse_args() try: from ubuntutools.config import ubu_email as get_maintainer except ImportError: get_maintainer = portable_get_maintainer if args.charmhome: charm_home = args.charmhome else: charm_home = os.getenv('CHARM_HOME', '.') home = path.abspath(path.dirname(__file__)) template_dir = path.join(home, 'templates') output_dir = path.join(charm_home, args.charmname) print "Generating template for " + args.charmname + " from templates in " \ + template_dir print "Charm will be stored in " + output_dir if path.exists(output_dir): print output_dir + " exists. Please move it out of the way." sys.exit(1) shutil.copytree(path.join(template_dir, 'charm'), output_dir) v = {'package': args.charmname, 'maintainer': '%s <%s>' % get_maintainer()} v.update(apt_fill(args.charmname)) for root, dirs, files in os.walk(output_dir): for outfile in files: full_outfile = path.join(root, outfile) mode = os.stat(full_outfile)[ST_MODE] try: t = Template(file=full_outfile, searchList=(v)) o = tempfile.NamedTemporaryFile(dir=root, delete=False) os.chmod(o.name, mode) o.write(str(t)) o.close() backupname = full_outfile + str(time.time()) os.rename(full_outfile, backupname) try: os.rename(o.name, full_outfile) os.unlink(backupname) except Exception, e: print "WARNING: Could not enable templated file: " + str(e) os.rename(backupname, full_outfile) raise except Exception, e: print "WARNING: could not process template for " \ + full_outfile + ": " + str(e) raise if __name__ == "__main__": main() charmtools-1.0.0/charmtools/getall.py0000664000175000017500000000461512214631411020107 0ustar marcomarco00000000000000#!/usr/bin/env python # Copyright (C) 2013 Marco Ceppi . # # 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 . import os import sys import argparse import subprocess from .mr import Mr from . import ext def setup_parser(): parser = argparse.ArgumentParser(prog='juju charm getall', description='Retrieves all charms from Launchpad') parser.add_argument('charms_directory', nargs='?', help='Path to where all charms should be downloaded') return parser def main(): parser = setup_parser() args = parser.parse_args() if not args.charms_directory: sys.stderr.write('Error: No value for charms_directory provided\n\n') parser.print_help() sys.exit(1) if not os.path.exists(args.charms_directory): os.makedirs(args.charms_directory, 0o755) charm_update = subprocess.call([os.path.join(os.path.dirname( os.path.realpath(sys.argv[0])), 'charm-update%s' % ext), args.charms_directory]) if charm_update != 0: sys.stderr.write('Unable to perform `juju charm update`!\n') sys.exit(1) try: mr = Mr(directory=args.charms_directory) sys.stderr.write('Grabbing %s charms from Charm Store\n' \ % len(mr.list())) for charm in mr.list(): sys.stderr.write('Branching %s\n' % charm) try: mr.update(charm) except (KeyboardInterrupt, SystemExit): sys.stderr.write('\nKeyboard Interrupt caught. Exiting!\n') break except Exception as e: print >> sys.stderr, "Error during update: ", e except Exception as e: print >> sys.stderr, "Error during setup: ", e charmtools-1.0.0/charmtools/search.py0000664000175000017500000000231312214343031020073 0ustar marcomarco00000000000000#!/usr/bin/env python # Copyright (C) 2013 Marco Ceppi . # # 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 . import os import sys import re import argparse from . import charms def setup_parser(): parser = argparse.ArgumentParser(prog='charm search', description='Match name against all charms (official and personal) in \ store') parser.add_argument('name', nargs=1, help='Name which to search by') return parser def main(): parser = setup_parser() args = parser.parse_args() matches = [c for c in charms.remote() if args.name[0] in c] print '\n'.join(matches) charmtools-1.0.0/charmtools/list.py0000664000175000017500000000174712214343031017613 0ustar marcomarco00000000000000#!/usr/bin/env python # Copyright (C) 2013 Marco Ceppi . # # 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 . import sys from . import charms def main(): if len(sys.argv) > 1: print('usage: list [ --help ]') if sys.argv[1] == '--help': sys.exit(0) else: sys.exit(1) print "\n".join(charms.remote()) if __name__ == "__main__": main() charmtools-1.0.0/charmtools/proof.py0000775000175000017500000003325512220131351017764 0ustar marcomarco00000000000000#!/usr/bin/python # Copyright (C) 2011 - 2012 Canonical Ltd. # # 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 . import argparse import email.utils import os import re from stat import ST_MODE from stat import S_IXUSR import sys import yaml import hashlib KNOWN_METADATA_KEYS = ['name', 'summary', 'maintainer', 'description', 'categories', 'subordinate', 'provides', 'requires', 'format', 'peers'] KNOWN_RELATION_KEYS = ['interface', 'scope', 'limit', 'optional'] KNOWN_SCOPES = ['global', 'container'] TEMPLATE_PATH = os.path.abspath(os.path.dirname(__file__)) TEMPLATE_README = os.path.join(TEMPLATE_PATH, 'templates', 'charm', 'README.ex') TEMPLATE_ICON = os.path.join(TEMPLATE_PATH, 'templates', 'charm', 'icon.svg') class RelationError(Exception): pass class Linter(object): def __init__(self): self.lint = [] self.exit_code = 0 def crit(self, msg): """Called when checking cannot continue.""" self.err("FATAL: " + msg) def err(self, msg): global EXIT_CODE self.lint.append("E: " + msg) if self.exit_code < 200: self.exit_code = 200 def info(self, msg): """Ignorable but sometimes useful.""" self.lint.append("I: " + msg) def warn(self, msg): global EXIT_CODE self.lint.append("W: " + msg) if self.exit_code < 100: self.exit_code = 100 def check_hook(self, hook, hooks_path, required=True, recommended=False): hook_path = os.path.join(hooks_path, hook) try: mode = os.stat(hook_path)[ST_MODE] if not mode & S_IXUSR: self.warn(hook + " not executable") with open(hook_path, 'r') as hook_file: count = 0 for line in hook_file: count += 1 hook_warnings = [ {'re': re.compile("http://169\.254\.169\.254/"), 'msg': "hook accesses EC2 metadata service directly"}] for warning in hook_warnings: if warning['re'].search(line): self.warn( "(%s:%d) - %s" % (hook, count, warning['msg'])) return True except OSError: if required: self.err("missing hook " + hook) elif recommended: self.warn("missing recommended hook " + hook) return False def check_relation_hooks(self, relations, subordinate, hooks_path): template_interfaces = ('interface-name') template_relations = ('relation-name') for r in relations.items(): if type(r[1]) != dict: self.err("relation %s is not a map" % (r[0])) else: if 'scope' in r[1]: scope = r[1]['scope'] if scope not in KNOWN_SCOPES: self.err("Unknown scope found in relation %s - (%s)" % (r[0], scope)) if 'interface' in r[1]: interface = r[1]['interface'] if interface in template_interfaces: self.err("template interface names should be " "changed: " + interface) else: self.err("relation missing interface") for key in r[1].keys(): if key not in KNOWN_RELATION_KEYS: self.err( "Unknown relation field in relation %s - (%s)" % (r[0], key)) r = r[0] if r in template_relations: self.err("template relations should be renamed to fit " "charm: " + r) has_one = False has_one = has_one or self.check_hook( r + '-relation-changed', hooks_path, required=False) has_one = has_one or self.check_hook( r + '-relation-departed', hooks_path, required=False) has_one = has_one or self.check_hook( r + '-relation-joined', hooks_path, required=False) has_one = has_one or self.check_hook( r + '-relation-broken', hooks_path, required=False) if not has_one and not subordinate: self.info("relation " + r + " has no hooks") def get_args(): parser = argparse.ArgumentParser( description='Performs static analysis on charms') parser.add_argument('charm_name', nargs='?', help='path of charm dir to check. Defaults to PWD') args = parser.parse_args() if args.charm_name: charm_name = args.charm_name else: charm_name = os.getcwd() return charm_name def run(charm_name): lint = Linter() if os.path.isdir(charm_name): charm_path = charm_name else: charm_home = os.getenv('CHARM_HOME', '.') charm_path = os.path.join(charm_home, charm_name) if not os.path.isdir(charm_path): lint.crit("%s is not a directory, Aborting" % charm_path) return lint.lint, lint.exit_code hooks_path = os.path.join(charm_path, 'hooks') yaml_path = os.path.join(charm_path, 'metadata.yaml') try: yamlfile = open(yaml_path, 'r') try: charm = yaml.load(yamlfile) except Exception as e: lint.crit('cannot parse ' + yaml_path + ":" + str(e)) return lint.lint, lint.exit_code yamlfile.close() for key in charm.keys(): if key not in KNOWN_METADATA_KEYS: lint.err("Unknown root metadata field (%s)" % key) charm_basename = os.path.basename(charm_path) if charm['name'] != charm_basename: warn_msg = ("metadata name (%s) must match directory name (%s) " "exactly for local deployment.") % ( charm['name'], charm_basename) lint.warn(warn_msg) # summary should be short if len(charm['summary']) > 72: lint.warn('summary sould be less than 72') # need a maintainer field if 'maintainer' not in charm: lint.err('Charms need a maintainer (See RFC2822) - Name ') else: if type(charm['maintainer']) == list: # It's a list maintainers = charm['maintainer'] else: maintainers = [charm['maintainer']] for maintainer in maintainers: (name, address) = email.utils.parseaddr(maintainer) formatted = email.utils.formataddr((name, address)) if formatted != maintainer: warn_msg = ("Maintainer address should contain a " "real-name and email only. [%s]" % ( formatted)) lint.warn(warn_msg) if 'categories' not in charm: lint.warn('Metadata is missing categories.') else: categories = charm['categories'] if type(categories) != list or categories == []: # The category names are not validated because jujucharms.com # may change them. lint.warn( 'Categories metadata must be a list of one or more of: ' 'applications, app-servers, databases, file-servers, ' 'cache-proxy, misc') if not os.path.exists(os.path.join(charm_path, 'icon.svg')): lint.warn("No icon.svg file.") else: # should have an icon.svg template_sha1 = hashlib.sha1() icon_sha1 = hashlib.sha1() try: with open(TEMPLATE_ICON) as ti: template_sha1.update(ti.read()) with open(os.path.join(charm_path, 'icon.svg')) as ci: icon_sha1.update(ci.read()) if template_sha1.hexdigest() == icon_sha1.hexdigest(): lint.err("Includes template icon.svg file.") except IOError as e: lint.err( "Error while opening %s (%s)" % (e.filename, e.strerror)) # Must have a hooks dir if not os.path.exists(hooks_path): lint.err("no hooks directory") # Must have a copyright file if not os.path.exists(os.path.join(charm_path, 'copyright')): lint.err("no copyright file") # should have a readme root_files = os.listdir(charm_path) found_readmes = set() for filename in root_files: if filename.upper().find('README') != -1: found_readmes.add(filename) if len(found_readmes): if 'README.ex' in found_readmes: lint.err("Includes template README.ex file") try: with open(TEMPLATE_README) as tr: bad_lines = [] for line in tr: if len(line) >= 25: bad_lines.append(line.strip()) for readme in found_readmes: readme_path = os.path.join(charm_path, readme) with open(readme_path) as r: readme_content = r.read() lc = 0 for l in bad_lines: if not len(l): continue lc += 1 if l in readme_content: err_msg = ('%s Includes boilerplate ' 'README.ex line %d') lint.err(err_msg % (readme, lc)) except IOError as e: lint.err( "Error while opening %s (%s)" % (e.filename, e.strerror)) else: lint.warn("no README file") subordinate = charm.get('subordinate', False) if type(subordinate) != bool: lint.err("subordinate must be a boolean value") # All charms should provide at least one thing provides = charm.get('provides') if provides is not None: lint.check_relation_hooks(provides, subordinate, hooks_path) else: if not subordinate: lint.warn("all charms should provide at least one thing") if subordinate: try: requires = charm.get('requires') if requires is not None: found_scope_container = False for rel_name, rel in requires.iteritems(): if 'scope' in rel: if rel['scope'] == 'container': found_scope_container = True break if not found_scope_container: raise RelationError else: raise RelationError except RelationError: lint.err("subordinates must have at least one scope: " "container relation") else: requires = charm.get('requires') if requires is not None: lint.check_relation_hooks(requires, subordinate, hooks_path) peers = charm.get('peers') if peers is not None: lint.check_relation_hooks(peers, subordinate, hooks_path) if 'revision' in charm: lint.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): lint.warn("revision should be a positive integer") lint.check_hook('install', hooks_path) lint.check_hook('start', hooks_path, required=False, recommended=True) lint.check_hook('stop', hooks_path, required=False, recommended=True) lint.check_hook('config-changed', hooks_path, required=False) except IOError: lint.err("could not find metadata file for " + charm_name) lint.exit_code = -1 rev_path = os.path.join(charm_path, 'revision') if not os.path.exists(rev_path): lint.err("revision file in root of charm is required") else: with open(rev_path, 'r') as rev_file: content = rev_file.read().rstrip() try: int(content) except ValueError: lint.err("revision file contains non-numeric data") return lint.lint, lint.exit_code def main(): charm_name = get_args() lint, exit_code = run(charm_name) if lint: print "\n".join(lint) sys.exit(exit_code) if __name__ == "__main__": main() charmtools-1.0.0/charmtools/promulgate.py0000664000175000017500000002062412214343031021012 0ustar marcomarco00000000000000#!/usr/bin/python # # promulgate - makes a charm recipe branch the official one # # Copyright (C) 2011 Canonical Ltd. # Author: Francis J. Lacoste # # 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 . # from launchpadlib.launchpad import Launchpad from lazr.restfulclient.errors import BadRequest, NotFound import os import sys import string from optparse import OptionParser from bzrlib import bzrdir from bzrlib.plugins.launchpad import lp_api import yaml import logging import subprocess DISTRIBUTION = 'charms' REVIEW_TEAM_NAME = 'charmers' OFFICIAL_BRANCH_POCKET = 'Release' OFFICIAL_BRANCH_STATUS = 'Mature' def parse_options(): parser = OptionParser(usage='usage: %prog [options] ') parser.add_option( '-b', '--branch', dest='branch', default=None, help='The location of the charm public branch. Will be determined ' 'from the bzr configuration if omitted.') parser.add_option( '-s', '--series', dest='series', default=None, help='The distribution series on which to set the official branch. ' 'Defaults to setting it in the current development series.') parser.add_option( '-t', '--lp-instance', dest='lp_instance', default='production', help="The Launchpad instance to use. Defaults to production, but " "staging' or 'qastaging' might be used for testing.") parser.add_option( '-v', '--verbose', dest='verbose', action='count', default=0, help='Increase verbosity level.') parser.add_option( '-u', '--unpromulgate', dest='unpromulgate', action='store_true', default=False, help='Un-promulgate this branch instead of promulgating it') parser.add_option( '-f', '--force', dest='force', action='store_true', default=False, help='Override warnings and errors. USE WITH EXTREME CARE !!!!') parser.add_option( '-w', '--ignore-warnings', dest='ignore_warnings', action='store_true', default=False, help='Promulgate this branch even with warnings from charm proof') parser.add_option( '-o', '--owner-branch', dest='promulgate_owner_branch', action='store_true', default=False, help='Promulgate a branch owned by a someone/group other than ' '~charmers') return parser.parse_args() def log_level(verbose): if verbose >= 2: return logging.DEBUG elif verbose >= 1: return logging.INFO else: return logging.WARNING def proof_charm(charm_dir, force=False, ignore_warnings=False): logging.info("Running charm proof ... ") charm_proof = subprocess.call([sys.executable, os.path.join( os.path.dirname(os.path.realpath(__file__)), 'proof'), charm_dir]) if charm_proof == 1 or charm_proof == 200: if force: logging.info("force option enabled ... Continuing with errors") else: sys.exit(1) if charm_proof == 100: if ignore_warnings: logging.info("ignore-warnings enabled ... Continuing with " "warnings") elif force: logging.info("force option enabled ... Continuing with warnings") else: sys.exit(charm_proof) if charm_proof == 0: logging.info("Excellent ... charm proof passed with flying colors") def charm_name_from_metadata(charm_dir): charm_metadata = os.path.join(charm_dir, 'metadata.yaml') if not os.access(charm_metadata, os.R_OK): logging.error("can't read charm metadata: %s", charm_metadata) with open(charm_metadata) as metadata: charm = yaml.load(metadata) return charm['name'] def find_branch_to_promulgate(lp, charm_dir, branch_url): if branch_url is None: tree, branch, relpath = bzrdir.BzrDir.open_containing_tree_or_branch( charm_dir) push_location = branch.get_push_location() if push_location is None: logging.error("Branch has not been pushed.") return 1 charm_branch = lp.branches.getByUrl(url=push_location) if charm_branch is None: logging.error("can't determine Launchpad branch from bzr branch") return 1 else: charm_branch = lp.branches.getByUrl(url=branch_url) if charm_branch is None: logging.error("can't find branch on Launchpad: %s", branch_url) return 1 return charm_branch def get_lp_charm_series(lp, series): charm_distro = lp.distributions[DISTRIBUTION] if series is None: charm_series = charm_distro.current_series else: try: charm_series = charm_distro.getSeries( name_or_version=series) except (BadRequest, NotFound), e: # XXX flacoste 2011-06-15 bug=797917 # Should only be NotFound. if e.content.startswith('No such distribution series:'): logging.error("can't find series '%s'", series) raise else: raise return charm_series def update_branch_info(charm_branch, branch_status, branch_reviewer): logging.info("Setting status of %s to %s", charm_branch.bzr_identity, branch_status) charm_branch.lifecycle_status = branch_status logging.info("Setting reviewer of %s to %s", charm_branch.bzr_identity, branch_reviewer) charm_branch.reviewer = branch_reviewer charm_branch.lp_save() def update_official_charm_branch(lp, series, charm_branch, charm_name): charm_series = get_lp_charm_series(lp, series) lp_charm = charm_series.getSourcePackage(name=charm_name) if charm_branch: logging.info('Setting %s as the official branch for %s', charm_branch.bzr_identity, lp_charm.name) update_branch_info(charm_branch, OFFICIAL_BRANCH_STATUS, lp.people[REVIEW_TEAM_NAME]) else: logging.info('Removing official branch for %s', lp_charm.name) lp_charm.setBranch(branch=charm_branch, pocket=OFFICIAL_BRANCH_POCKET) def branch_owner(bzr_branch): lp_url = bzr_branch.bzr_identity # TODO this really sucks... better way? return lp_url.lstrip('lp:').split('/')[0] def is_valid_owner(charm_branch, promulgate_owner_branch): if charm_branch is None: return True return promulgate_owner_branch or branch_owner(charm_branch) == '~charmers' def main(): options, args = parse_options() logging.basicConfig(level=log_level(options.verbose), format='%(levelname)s:%(message)s') if len(args): charm_dir = args[0] else: charm_dir = os.getcwd() proof_charm(charm_dir, options.force, options.ignore_warnings) logging.debug('login with %s launchpad:', options.lp_instance) lp = Launchpad.login_with('promulgate', options.lp_instance) if options.unpromulgate: logging.info('unpromulgating...') charm_branch = None # makes LP delete the source package. else: logging.info('promulgating...') charm_branch = find_branch_to_promulgate(lp, charm_dir, options.branch) if not is_valid_owner(charm_branch, options.promulgate_owner_branch): logging.error(" Invalid branch owner: %s", branch_owner(charm_branch)) logging.error(" Branch push location must be owned by '~charmers'\n" " use `bzr push --remember lp:~charmers/charms/" "//trunk`\n or override this " "behavior using the '--owner-branch'" " option") return 1 update_official_charm_branch(lp, options.series, charm_branch, charm_name_from_metadata(charm_dir)) return 0 if __name__ == '__main__': sys.exit(main()) charmtools-1.0.0/charmtools/subscribers.py0000664000175000017500000001661212214343031021163 0ustar marcomarco00000000000000#!/usr/bin/python # # subscribers - list and manipulate charm subscribers # # Copyright (C) 2012 Canonical Ltd. # Author: Clint Byrum # # 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 . # import os import sys import yaml import argparse import logging from logging import warn, info, debug from launchpadlib import launchpad def setup_parser(): parser = argparse.ArgumentParser( description="""Prints out a report and optionally corrects found instances where maintainers of charms are not subscribed to bugs on their charm in launchpad. Users will need all charms they are interested in checking locally in the specified repository so that metadata.yaml can be inspected to find the maintainer.""") parser.add_argument('--subscribed', default=False, action='store_true', help='Show maintainers who are properly subscribed ' 'instead of unsubscribed') parser.add_argument('--repository', default=None, type=str, help='Repository to look for charms in. Defaults to ' '$JUJU_REPOSITORY or getcwd') parser.add_argument('--quiet', default=False, action='store_true', help='Hide everything except maintainer subscription ' 'lists.') parser.add_argument('--series', '-s', default=None, help='Which series of the charm store to run against. ' 'Defaults to current dev focus') parser.add_argument('--maintainer', default=None, help='Limit output to this maintainer\'s charms only.') parser.add_argument('--log-priority', default='WARNING') parser.add_argument('--launchpad-instance', default='production') parser.add_argument('--fix-unsubscribed', default=False, action='store_true', help='Add a bug subscription for ' 'any unsubscribed maintainers. Requires --maintainer') parser.add_argument('--force-fix-all', default=False, action='store_true', help=argparse.SUPPRESS) parser.add_argument('charms', default=[], nargs='*', help='Charms to check for subscriptions') return parser def main(): parser = setup_parser() args = parser.parse_args() if args.repository is None: repository = os.environ.get('JUJU_REPOSITORY', os.getcwd()) else: repository = args.repository if args.log_priority == 'WARNING': log_prio = logging.WARNING elif args.log_priority == 'INFO': log_prio = logging.INFO elif args.log_priority == 'CRITICAL': log_prio = logging.CRITICAL elif args.log_priority == 'INFO': log_prio = logging.INFO elif args.log_priority == 'DEBUG': log_prio = logging.DEBUG else: log_prio = logging.DEBUG logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=log_prio) if args.quiet: logging.disable(logging.WARNING) if args.maintainer is None and args.fix_unsubscribed: warn('Running --fix-unsubscribed and without --maintainer is against' ' policy.') if not args.force_fix_all: warn('Use --force-fix-all to override policy') sys.exit(1) else: warn('--force-fix-all passed, continuing') app_name = 'Charm Tools - subscribers' home_dir = os.environ.get('HOME', os.path.expanduser('~')) launchpadlib_dir = os.path.join(home_dir, '.cache', 'launchpadlib') if args.fix_unsubscribed: login = launchpad.Launchpad.login_with else: login = launchpad.Launchpad.login_anonymously lp = login(app_name, args.launchpad_instance, version='devel', launchpadlib_dir=launchpadlib_dir) charmdistro = lp.distributions['charms'] if args.series: current_series = args.series else: current_series = str(charmdistro.current_series).split('/').pop() charms = [] if len(args.charms): for charm_name in args.charms: charm = charmdistro.getSourcePackage(name=charm_name) charms.append(charm_name) else: branches = charmdistro.getBranchTips() for branch in branches: try: branch_series = str(branch[2][0]).split('/')[0] charm_name = str(branch[0]).split('/')[3] except IndexError: continue if branch_series != current_series: continue charms.append(charm_name) for charm_name in charms: try: with open('%s/%s/%s/metadata.yaml' % (repository, current_series, charm_name)) as mdata: mdata_parsed = yaml.safe_load(mdata) except IOError: warn('%s/%s has no metadata in charm repo' % (current_series, charm_name)) continue try: maintainers = mdata_parsed['maintainer'] except KeyError: warn('%s has no maintainer' % charm_name) continue if type(maintainers) == str: maintainers = [maintainers] if args.maintainer is not None: if args.maintainer not in maintainers: maints_by_email = [m.split('<')[1].split('>')[0] for m in maintainers] if args.maintainer not in maints_by_email: debug('%s not in maintainer list %s' % (args.maintainer, maintainers)) continue for maintainer in maintainers: maint_email = maintainer.split('<')[1].split('>')[0] lp_maintainer = lp.people.getByEmail(email=maint_email) if not lp_maintainer: warn('%s has no people in launchpad' % maintainer) continue pkg = charmdistro.getSourcePackage(name=charm_name) subscription = pkg.getSubscription(person=lp_maintainer) if subscription is not None: msg = '%s is subscribed to %s' % (maintainer, charm_name) if args.subscribed: print msg else: info(msg) elif not args.subscribed: msg = '%s is not subscribed to %s' % (maintainer, charm_name) if args.subscribed: info(msg) else: print msg if args.fix_unsubscribed: info('adding bug subscription to %s for %s' % (charm_name, maint_email)) pkg.addBugSubscription(subscriber=lp_maintainer) charmtools-1.0.0/charmtools/charms.py0000664000175000017500000000201312214343031020100 0ustar marcomarco00000000000000import os import sys from launchpadlib.launchpad import Launchpad def remote(): lp = Launchpad.login_anonymously('charm-tools', 'production', version='devel') charm = lp.distributions['charms'] current_series = str(charm.current_series).split('/').pop() branches = charm.getBranchTips() charms = [] for branch in branches: try: branch_series = str(branch[2][0]).split('/')[0] charm_name = str(branch[0]).split('/')[3] except IndexError: branch_series = '' if branch_series == current_series: charms.append("lp:charms/%s" % charm_name) else: charms.append("lp:%s" % branch[0]) return charms def local(directory): '''Show charms that actually exist locally. Different than Mr.list''' local_charms = [] for charm in os.listdir(directory): if os.path.exists(os.join(directory, charm, '.bzr')): local_charms.append(charm) return local_charms charmtools-1.0.0/charmtools/review.py0000664000175000017500000000657412214343031020144 0ustar marcomarco00000000000000#!/usr/bin/python # # review - adds review comment to charm bug # # Copyright (C) 2011 Canonical Ltd. # Author: Mark Mims # # 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 . # import argparse import logging import sys from launchpadlib.launchpad import Launchpad def parse_options(): parser = argparse.ArgumentParser( description="Review a charm by adding a comment to the corresponding" " charm bug. The review comment can be passed as a --message" " arg or via stdin") parser.add_argument('bug_id', help='The launchpad bug for the charm being reviewed.') parser.add_argument( '--message', '-m', help='The review text to add as a comment to the bug.') parser.add_argument( '--yes', '-y', dest="skip_prompt", default=False, action='store_true', help='do not prompt me... just do it.') parser.add_argument( '--verbose', '-v', default=False, action='store_true', help='show debug logging?') parser.add_argument( '--lp-instance', '-t', dest='lp_instance', default='production', help="The Launchpad instance to use. Defaults to production, but " "staging' or 'qastaging' might be used for testing.") return parser.parse_args() def log_level(verbose): if verbose: return logging.DEBUG else: return logging.WARNING def get_message_from_stdin(): stream = sys.stdin text = stream.read() sys.stdin = open('/dev/tty') # 'reset' stdin to prompt if necessary return text def get_message(message): if message: return message else: return get_message_from_stdin() def prompt_to_continue(bug_id): logging.debug("prompting") ans = raw_input("Really add this comment to launchpad bug #%s? y/[n] " % bug_id) return ans.strip().lower().startswith('y') def main(): args = parse_options() logging.basicConfig(level=log_level(args.verbose), format='%(levelname)s:%(message)s') review_message = get_message(args.message) # before connecting to lp logging.debug('login with %s launchpad:', args.lp_instance) lp = Launchpad.login_with('charm-pilot', args.lp_instance) bug_id = args.bug_id logging.debug('find bug %s:', bug_id) bug = lp.bugs[bug_id] if bug: logging.debug('found bug') if args.skip_prompt or prompt_to_continue(bug_id): logging.debug('adding comment') # TODO check return or catch exception bug.newMessage(content=review_message) else: logging.debug('not adding comment') else: logging.error("no bug: %s", bug_id) return 1 return 0 if __name__ == '__main__': sys.exit(main()) charmtools-1.0.0/charmtools/__init__.py0000664000175000017500000000415612214637617020414 0ustar marcomarco00000000000000#!/usr/bin/env python # Copyright (C) 2013 Marco Ceppi . # # 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 . import os import sys import glob import subprocess ext = '' if os.name == 'nt': ext = '.exe' def usage(exit_code=0): sys.stderr.write('usage: %s subcommand\n' % sys.argv[0]) subs = subcommands(os.path.dirname(os.path.realpath(__file__))) sys.stderr.write('\n Available subcommands are:\n ') sys.stderr.write('\n '.join(subs)) sys.stderr.write('\n') sys.exit(exit_code) def subcommands(scripts_dir): subs = [] for path in os.environ['PATH'].split(os.pathsep): path = path.strip('"') for cmd in glob.glob(os.path.join(path, 'charm-*%s' % ext)): sub = os.path.basename(cmd) sub = sub.split('charm-')[1].replace(ext, '') subs.append(sub) subs = sorted(set(subs)) # Removes blacklisted items from the subcommands list. return filter(lambda s: s not in ['mr', 'charms'], subs) def main(): if len(sys.argv) < 2: usage(1) sub = sys.argv[1] opts = sys.argv[2:] if sub == '--description': sys.stdout.write("Set of tools for authoring and maintaining charms\n") sys.exit(0) sub_exec = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "charm-%s%s" % (sub, ext)) if not os.path.exists(sub_exec): sys.stderr.write('Error: %s is not a valid subcommand\n\n' % sub) usage(2) subprocess.call([sub_exec] + opts) if __name__ == '__main__': main() charmtools-1.0.0/charmtools/mr.py0000664000175000017500000001230012214343031017241 0ustar marcomarco00000000000000# Copyright (C) 2013 Marco Ceppi . # # 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 . import os import ConfigParser from bzrlib import errors, trace from bzrlib.bzrdir import BzrDir from bzrlib.branch import Branch from bzrlib.plugin import load_plugins from bzrlib.repository import Repository load_plugins() trace.enable_default_logging() # Provide better error handling class Mr: def __init__(self, directory=None, config=None, mr_compat=True): self.directory = directory or os.getcwd() self.control_dir = os.path.join(self.directory, '.bzr') self.config_file = config or os.path.join(self.directory, '.mrconfig') self.mr_compat = mr_compat if mr_compat: if self.__is_repository(): self.config = self.__read_cfg() self.bzr_dir = Repository.open(self.directory) else: self.config = ConfigParser.RawConfigParser() r = BzrDir.create(self.directory) self.bzr_dir = r.create_repository(shared=True) else: self.config = ConfigParser.RawConfigParser() self.bzr_dir = None def add(self, name, repository='lp:charms', checkout=False): # This isn't a true conversion of Mr, as such it's highly specialized # for just Charm Tools. So when you "add" a charm, it's just going # to use the charm name to fill in a template. Repository is in there # just in case we later add personal branching. '''Add a respository to the mrconfig''' if not name: raise Exception('No name provided') if not self.config.has_section(name): self.config.add_section(name) self.config.set(name, 'checkout', "bzr branch %s %s" % ('/'.join([repository, name]), name)) if checkout: self.checkout(name) def checkout(self, name=None): '''Checkout either one or all repositories from the mrconfig''' if not name: for name in self.config.sections(): charm_remote = self.__get_repository(name) self.__checkout(charm_remote, os.path.join(self.directory, name)) else: # Move this, and the charm_* stuff to _checkout? Makes sense if not self.config.has_section(name): raise Exception('No configuration for %s' % name) charm_remote = self.__get_repository(name) self.__checkout(charm_remote, os.path.join(self.directory, name)) def update(self, name=None, force=False): '''Update, or checkout, a charm in to directory''' if name: self.__update(name) else: for charm in self.config.sections(): self.__update(charm) def remove(self, name=None): '''Remove a repository from the mrconfig''' if not name: raise Exception('No name provided') self.config.remove_section(name) def list(self): '''Return all sections of the mr configuration''' return self.config.sections() def exists(self, name): '''Checks if the configuration already exists for this section''' return self.config.has_section(name) def save(self): '''Save the configuration file to disk''' with open(self.config_file, 'w') as mrcfg: self.config.write(mrcfg) __write_cfg = save def __read_cfg(self): cfg = ConfigParser.ConfigParser() if not self.config_file: raise Exception('No .mrconfig specified') if os.path.exists(self.config_file): cfg.read(self.config_file) return cfg def __checkout(self, src, to): remote = Branch.open(src) remote.bzrdir.sprout(to) # I wish there was a way to 'close' a RemoteBranch. Sadly, # I don't think there is def __update(self, name): if not os.path.exists(os.path.join(self.directory, name, '.bzr')): return self.checkout(name) charm_remote = self.__get_repository(name) local_branch = Branch.open(os.path.join(self.directory, name)) remote_branch = Branch.open(charm_remote) local_branch.pull(remote_branch) def __get_repository(self, name): if not self.config.has_section(name): raise Exception('No section "%s" configured' % name) return self.config.get(name, 'checkout').split(' ')[-2] def __is_repository(self): try: r = Repository.open(self.directory) except: return False return r.is_shared() charmtools-1.0.0/charmtools/review_queue.py0000664000175000017500000001375312214343031021345 0ustar marcomarco00000000000000#!/usr/bin/env python from launchpadlib.launchpad import Launchpad from operator import itemgetter import datetime import itertools import argparse def calculate_age(from_date=None): if not from_date: return None def format_as_table(data, keys, header=None, sort_by_key=None, sort_order_reverse=False): """Takes a list of dictionaries, formats the data, and returns the formatted data as a text table. Required Parameters: data - Data to process (list of dictionaries). (Type: List) keys - List of keys in the dictionary. (Type: List) Optional Parameters: header - The table header. (Type: List) sort_by_key - The key to sort by. (Type: String) sort_order_reverse - Default sort order is ascending, if True sort order will change to descending. (Type: Boolean) """ # Sort the data if a sort key is specified (default sort order # is ascending) if sort_by_key: data = sorted(data, key=itemgetter(sort_by_key), reverse=sort_order_reverse) # If header is not empty, add header to data if header: # Get the length of each header and create a divider based # on that length header_divider = [] for name in header: header_divider.append('-' * len(name)) # Create a list of dictionary from the keys and the header and # insert it at the beginning of the list. Do the same for the # divider and insert below the header. header_divider = dict(zip(keys, header_divider)) data.insert(0, header_divider) header = dict(zip(keys, header)) data.insert(0, header) column_widths = [] for key in keys: column_widths.append(max(len(str(column[key])) for column in data)) # Create a tuple pair of key and the associated column width for it key_width_pair = zip(keys, column_widths) format = ('%-*s ' * len(keys)).strip() + '\n' formatted_data = '' for element in data: data_to_format = [] # Create a tuple that will be used for the formatting in # width, value format for pair in key_width_pair: data_to_format.append(pair[1]) data_to_format.append(element[pair[0]]) formatted_data += format % tuple(data_to_format) return formatted_data def charm_review_queue(): print "Connecting to launchpad..." lp = Launchpad.login_anonymously('charm-tools', 'production', version='devel', launchpadlib_dir='~/.cache/launchpadlib') charm = lp.distributions['charms'] charmers = lp.people['charmers'] charm_contributors = lp.people['charm-contributors'] print "Querying launchpad for bugs ..." bugs = charm.searchTasks(tags=['new-formula', 'new-charm'], status=['New', 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed'], tags_combinator="Any") charmers_bugs = charmers.searchTasks( status=['New', 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed']) print "Querying launchpad for proposals ..." proposals = charmers.getRequestedReviews(status="Needs review") charm_contributors_proposals = charm_contributors.getRequestedReviews( status="Needs review") print "Building review_queue ..." queue = list() max_summary_length = 50 # Bugs in charms distribution and charmers group for bug in itertools.chain(bugs.entries, charmers_bugs.entries): entry_summary = bug['title'].split('"')[1].strip() bug_created = datetime.datetime.strptime( bug['date_created'].split('+')[0], "%Y-%m-%dT%H:%M:%S.%f") entry_age = datetime.datetime.utcnow() - bug_created entry = {'date_created': bug['date_created'].split("T")[0], 'age': str(entry_age).split('.')[0], 'summary': (entry_summary[:max_summary_length] + '...') if len(entry_summary) > max_summary_length else entry_summary, 'item': bug['web_link'], 'status': bug['status'], } queue.append(entry) # Merge proposals in charmers group for proposal in itertools.chain(proposals.entries, charm_contributors_proposals.entries): proposal_summary = proposal['description'] proposal_date_created = datetime.datetime.strptime( proposal['date_created'].split('+')[0], "%Y-%m-%dT%H:%M:%S.%f") proposal_age = datetime.datetime.utcnow() - proposal_date_created if proposal_summary is None: proposal_summary = "Proposal" entry = {'date_created': proposal['date_created'].split("T")[0], 'age': str(proposal_age).split('.')[0], 'summary': (proposal_summary[:max_summary_length] + '...') if len(proposal_summary) > max_summary_length else proposal_summary, 'item': proposal['web_link'], 'status': proposal['queue_status'], } queue.append(entry) return(sorted(queue, key=lambda k: k['date_created'])) def main(): parser = argparse.ArgumentParser( description="Shows items needing the attention of ~charmers") parser.parse_args() review_queue = charm_review_queue() keys = ['date_created', 'age', 'summary', 'item', 'status'] headers = ['Date Created', 'Age', 'Summary', 'Item', 'Status'] print "Queue length: %d" % len(review_queue) if isinstance(review_queue, list) and len(review_queue) > 0: print format_as_table(review_queue, keys, header=headers, sort_by_key='date_created', sort_order_reverse=False) if __name__ == "__main__": main() charmtools-1.0.0/charmtools/update.py0000664000175000017500000000352512214343031020116 0ustar marcomarco00000000000000#!/usr/bin/env python # Copyright (C) 2013 Marco Ceppi . # # 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 . import os import sys import re import argparse from . import charms from .mr import Mr def setup_parser(): parser = argparse.ArgumentParser(prog='charm update', description='Update charm_directory with latest from Charm Store') parser.add_argument('charm_directory', nargs='?', help='Path to where all charms are stored') parser.add_argument('-f', '--fix', action='store_true', help='Attempt to fix charms in charm_directory') return parser def main(): parser = setup_parser() args = parser.parse_args() sys.stderr.write('Pulling charm list from Launchpad\n') mr = Mr(args.charm_directory) for charm in charms.remote(): if re.match('^lp:charms\/', charm): charm_name = os.path.basename(charm) if mr.exists(charm_name) and args.fix: mr.update(charm_name, force=True) continue mr.add(charm_name) try: mr.save() except Exception as e: # Got this from http://stackoverflow.com/q/5574702/196832 print >> sys.stderr, ".mrconfig not saved: ", e sys.exit(1) charmtools-1.0.0/charmtools/templates/0000775000175000017500000000000012220131557020260 5ustar marcomarco00000000000000charmtools-1.0.0/charmtools/templates/charm/0000775000175000017500000000000012220131557021352 5ustar marcomarco00000000000000charmtools-1.0.0/charmtools/templates/charm/icon.svg0000664000175000017500000002361312214343031023023 0ustar marcomarco00000000000000 image/svg+xml charmtools-1.0.0/charmtools/templates/charm/README.ex0000664000175000017500000000324612214343031022645 0ustar marcomarco00000000000000Describe the intended usage of this charm and anything unique about how this charm relates to others here. This README will be displayed in the Charm Store, it should be either Markdown or RST. Ideal READMEs include instructions on how to use the charm, expected usage, and charm features that your audience might be interested in. For an example of a well written README check out Hadoop: http://jujucharms.com/charms/precise/hadoop Here's an example you might wish to template off of: Overview -------- This charm provides (service) from (service homepage). Add a description here of what the service itself actually does. Usage ----- Step by step instructions on using the charm: juju deploy servicename and so on. If you're providing a web service or something that the end user needs to go to, tell them here, especially if you're deploying a service that might listen to a non-default port. You can then browse to http://ip-address to configure the service. Configuration ------------- The configuration options will be listed on the charm store, however If you're making assumptions or opinionated decisions in the charm (like setting a default administrator password), you should detail that here so the user knows how to change it immediately, etc. Contact Information ------------------- Though this will be listed in the charm store itself don't assume a user will know that, so include that information here: Author: Report bugs at: http://bugs.launchpad.net/charms/+source/charmname Location: http://jujucharms.com/charms/distro/charmname * Be sure to remove the templated parts before submitting to https://launchpad.net/charms for inclusion in the charm store. charmtools-1.0.0/charmtools/templates/charm/hooks/0000775000175000017500000000000012220131557022475 5ustar marcomarco00000000000000charmtools-1.0.0/charmtools/templates/charm/hooks/stop0000775000175000017500000000054312214343031023405 0ustar marcomarco00000000000000#!/bin/bash # This will be run when the service is being torn down, allowing you to disable # it in various ways.. # For example, if your web app uses a text file to signal to the load balancer # that it is live... you could remove it and sleep for a bit to allow the load # balancer to stop sending traffic. # rm /srv/webroot/server-live.txt && sleep 30 charmtools-1.0.0/charmtools/templates/charm/hooks/relation-name-relation-broken0000775000175000017500000000013012214343031030234 0ustar marcomarco00000000000000#!/bin/sh # This hook runs when the full relation is removed (not just a single member) charmtools-1.0.0/charmtools/templates/charm/hooks/relation-name-relation-changed0000775000175000017500000000053112214343031030352 0ustar marcomarco00000000000000#!/bin/bash # # This must be renamed to the name of the relation. The goal here is to # affect any change needed by relationships being formed, modified, or broken # This script should be idempotent. #raw juju-log $JUJU_REMOTE_UNIT modified its settings juju-log Relation settings: relation-get juju-log Relation members: relation-list #end raw charmtools-1.0.0/charmtools/templates/charm/hooks/upgrade-charm0000775000175000017500000000035512214343031025140 0ustar marcomarco00000000000000#!/bin/bash # This hook is executed each time a charm is upgraded after the new charm # contents have been unpacked # # Best practice suggests you execute the hooks/install and # hooks/config-changed to ensure all updates are processed charmtools-1.0.0/charmtools/templates/charm/hooks/relation-name-relation-joined0000775000175000017500000000033712214343031030235 0ustar marcomarco00000000000000#!/bin/sh # This must be renamed to the name of the relation. The goal here is to # affect any change needed by relationships being formed # This script should be idempotent. #raw juju-log $JUJU_REMOTE_UNIT joined #end raw charmtools-1.0.0/charmtools/templates/charm/hooks/relation-name-relation-departed0000775000175000017500000000036012214343031030551 0ustar marcomarco00000000000000#!/bin/sh # This must be renamed to the name of the relation. The goal here is to # affect any change needed by the remote unit leaving the relationship. # This script should be idempotent. #raw juju-log $JUJU_REMOTE_UNIT departed #end raw charmtools-1.0.0/charmtools/templates/charm/hooks/start0000775000175000017500000000023512214343031023553 0ustar marcomarco00000000000000#!/bin/bash # Here put anything that is needed to start the service. # Note that currently this is run directly after install # i.e. 'service apache2 start' charmtools-1.0.0/charmtools/templates/charm/hooks/install0000775000175000017500000000057312214343031024071 0ustar marcomarco00000000000000#!/bin/bash # # Here do anything needed to install the service # i.e. apt-get install -y foo or bzr branch http://myserver/mycode /srv/webroot # # Make sure this hook exits cleanly and is idempotent, common problems here are # failing to account for a debconf question on a dependency, or trying to pull # from github without installing git first. apt-get install -y $package charmtools-1.0.0/charmtools/templates/charm/hooks/config-changed0000775000175000017500000000013612214343031025252 0ustar marcomarco00000000000000#!/bin/bash # config-changed occurs everytime a new configuration value is updated (juju set) charmtools-1.0.0/charmtools/templates/charm/revision0000664000175000017500000000000212214343031023116 0ustar marcomarco000000000000001 charmtools-1.0.0/charmtools/templates/charm/metadata.yaml0000664000175000017500000000045712214343031024017 0ustar marcomarco00000000000000name: $package summary: $summary maintainer: $maintainer description: | $description categories: - misc subordinate: false provides: provides-relation: interface: interface-name requires: requires-relation: interface: interface-name peers: peer-relation: interface: interface-name charmtools-1.0.0/charmtools/templates/charm/config.yaml0000664000175000017500000000056712214343031023506 0ustar marcomarco00000000000000options: string-option: type: string default: "Default Value" description: "A short description of the configuration option" boolean-option: type: boolean default: False description: "A short description of the configuration option" int-option: type: int default: 9001 description: "A short description of the configuration option" charmtools-1.0.0/charmtools/get.py0000664000175000017500000000376612214343031017422 0ustar marcomarco00000000000000#!/usr/bin/env python # Copyright (C) 2013 Marco Ceppi . # # 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 . import os import sys import argparse from .mr import Mr from . import charms def setup_parser(): parser = argparse.ArgumentParser(prog='charm get', description='Retrieves official charm branch from launchpad.net') parser.add_argument('charm', nargs=1, help='Charm to branch', metavar=('charm_name')) parser.add_argument('branch_to', nargs='?', help='Path to where charm should be branched') return parser def main(): parser = setup_parser() args = parser.parse_args() charm = args.charm[0] ldir = args.branch_to branch_dir = os.path.realpath(ldir) if ldir else os.getcwd() if "lp:charms/%s" % charm not in charms.remote(): sys.stderr.write('Error: %s not found in charm store.\n' % charm) sys.exit(1) charm_dir = os.path.join(branch_dir, charm) if os.path.exists(charm_dir) and os.listdir(charm_dir): sys.stderr.write('Error: %s exists and is not empty\n' % charm_dir) if not os.path.exists(branch_dir): os.makedirs(branch_dir) try: mr = Mr(branch_dir, mr_compat=False) sys.stderr.write('Branching %s to %s\n' % (charm, branch_dir)) mr.add(charm, checkout=True) except Exception as e: print >> sys.stderr, "Error during branching: ", e charmtools-1.0.0/MANIFEST.in0000664000175000017500000000006312220131540015634 0ustar marcomarco00000000000000include *.py README recursive-include charmtools *