3
# Copyright 2014-2015 Canonical Limited.
5
# This file is part of charm-helpers.
7
# charm-helpers is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Lesser General Public License version 3 as
9
# published by the Free Software Foundation.
11
# charm-helpers is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU Lesser General Public License for more details.
16
# You should have received a copy of the GNU Lesser General Public License
17
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
20
# Adam Gandelman <adamg@ubuntu.com>
30
from fnmatch import fnmatch
34
CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
37
def parse_config(conf_file):
38
if not os.path.isfile(conf_file):
39
logging.error('Invalid config file: %s.' % conf_file)
41
return yaml.load(open(conf_file).read())
44
def clone_helpers(work_dir, branch):
45
dest = os.path.join(work_dir, 'charm-helpers')
46
logging.info('Checking out %s to %s.' % (branch, dest))
47
cmd = ['bzr', 'checkout', '--lightweight', branch, dest]
48
subprocess.check_call(cmd)
52
def _module_path(module):
53
return os.path.join(*module.split('.'))
56
def _src_path(src, module):
57
return os.path.join(src, 'charmhelpers', _module_path(module))
60
def _dest_path(dest, module):
61
return os.path.join(dest, _module_path(module))
65
return os.path.isfile(path + '.py')
68
def ensure_init(path):
70
ensure directories leading up to path are importable, omitting
71
parent directory, eg path='/hooks/helpers/foo'/:
73
hooks/helpers/__init__.py
74
hooks/helpers/foo/__init__.py
76
for d, dirs, files in os.walk(os.path.join(*path.split('/')[:2])):
77
_i = os.path.join(d, '__init__.py')
78
if not os.path.exists(_i):
79
logging.info('Adding missing __init__.py: %s' % _i)
80
open(_i, 'wb').close()
83
def sync_pyfile(src, dest):
85
src_dir = os.path.dirname(src)
86
logging.info('Syncing pyfile: %s -> %s.' % (src, dest))
87
if not os.path.exists(dest):
89
shutil.copy(src, dest)
90
if os.path.isfile(os.path.join(src_dir, '__init__.py')):
91
shutil.copy(os.path.join(src_dir, '__init__.py'),
96
def get_filter(opts=None):
99
# do not filter any files, include everything
102
def _filter(dir, ls):
103
incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt]
106
_f = os.path.join(dir, f)
108
if not os.path.isdir(_f) and not _f.endswith('.py') and incs:
109
if True not in [fnmatch(_f, inc) for inc in incs]:
110
logging.debug('Not syncing %s, does not match include '
111
'filters (%s)' % (_f, incs))
114
logging.debug('Including file, which matches include '
115
'filters (%s): %s' % (incs, _f))
116
elif (os.path.isfile(_f) and not _f.endswith('.py')):
117
logging.debug('Not syncing file: %s' % f)
119
elif (os.path.isdir(_f) and not
120
os.path.isfile(os.path.join(_f, '__init__.py'))):
121
logging.debug('Not syncing directory: %s' % f)
127
def sync_directory(src, dest, opts=None):
128
if os.path.exists(dest):
129
logging.debug('Removing existing directory: %s' % dest)
131
logging.info('Syncing directory: %s -> %s.' % (src, dest))
133
shutil.copytree(src, dest, ignore=get_filter(opts))
137
def sync(src, dest, module, opts=None):
139
# Sync charmhelpers/__init__.py for bootstrap code.
140
sync_pyfile(_src_path(src, '__init__'), dest)
142
# Sync other __init__.py files in the path leading to module.
144
steps = module.split('.')[:-1]
146
m.append(steps.pop(0))
147
init = '.'.join(m + ['__init__'])
148
sync_pyfile(_src_path(src, init),
149
os.path.dirname(_dest_path(dest, init)))
151
# Sync the module, or maybe a .py file.
152
if os.path.isdir(_src_path(src, module)):
153
sync_directory(_src_path(src, module), _dest_path(dest, module), opts)
154
elif _is_pyfile(_src_path(src, module)):
155
sync_pyfile(_src_path(src, module),
156
os.path.dirname(_dest_path(dest, module)))
158
logging.warn('Could not sync: %s. Neither a pyfile or directory, '
159
'does it even exist?' % module)
162
def parse_sync_options(options):
165
return options.split(',')
168
def extract_options(inc, global_options=None):
169
global_options = global_options or []
170
if global_options and isinstance(global_options, six.string_types):
171
global_options = [global_options]
173
return (inc, global_options)
174
inc, opts = inc.split('|')
175
return (inc, parse_sync_options(opts) + global_options)
178
def sync_helpers(include, src, dest, options=None):
179
if not os.path.isdir(dest):
182
global_options = parse_sync_options(options)
185
if isinstance(inc, str):
186
inc, opts = extract_options(inc, global_options)
187
sync(src, dest, inc, opts)
188
elif isinstance(inc, dict):
189
# could also do nested dicts here.
190
for k, v in six.iteritems(inc):
191
if isinstance(v, list):
193
inc, opts = extract_options(m, global_options)
194
sync(src, dest, '%s.%s' % (k, inc), opts)
196
if __name__ == '__main__':
197
parser = optparse.OptionParser()
198
parser.add_option('-c', '--config', action='store', dest='config',
199
default=None, help='helper config file')
200
parser.add_option('-D', '--debug', action='store_true', dest='debug',
201
default=False, help='debug')
202
parser.add_option('-b', '--branch', action='store', dest='branch',
203
help='charm-helpers bzr branch (overrides config)')
204
parser.add_option('-d', '--destination', action='store', dest='dest_dir',
205
help='sync destination dir (overrides config)')
206
(opts, args) = parser.parse_args()
209
logging.basicConfig(level=logging.DEBUG)
211
logging.basicConfig(level=logging.INFO)
214
logging.info('Loading charm helper config from %s.' % opts.config)
215
config = parse_config(opts.config)
217
logging.error('Could not parse config from %s.' % opts.config)
222
if 'branch' not in config:
223
config['branch'] = CHARM_HELPERS_BRANCH
225
config['branch'] = opts.branch
227
config['destination'] = opts.dest_dir
229
if 'destination' not in config:
230
logging.error('No destination dir. specified as option or config.')
233
if 'include' not in config:
235
logging.error('No modules to sync specified as option or config.')
237
config['include'] = []
238
[config['include'].append(a) for a in args]
241
if 'options' in config:
242
sync_options = config['options']
243
tmpd = tempfile.mkdtemp()
245
checkout = clone_helpers(tmpd, config['branch'])
246
sync_helpers(config['include'], checkout, config['destination'],
247
options=sync_options)
248
except Exception as e:
249
logging.error("Could not sync: %s" % e)
252
logging.debug('Cleaning up %s' % tmpd)