~corey.bryant/charms/trusty/ceilometer-agent/action-managed-upgrade

« back to all changes in this revision

Viewing changes to bin/charm_helpers_sync.py

  • Committer: Corey Bryant
  • Date: 2015-09-15 17:03:04 UTC
  • Revision ID: corey.bryant@canonical.com-20150915170304-loy9j21jdnil5bqo
DropĀ binĀ file

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
 
3
 
# Copyright 2014-2015 Canonical Limited.
4
 
#
5
 
# This file is part of charm-helpers.
6
 
#
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.
10
 
#
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.
15
 
#
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/>.
18
 
 
19
 
# Authors:
20
 
#   Adam Gandelman <adamg@ubuntu.com>
21
 
 
22
 
import logging
23
 
import optparse
24
 
import os
25
 
import subprocess
26
 
import shutil
27
 
import sys
28
 
import tempfile
29
 
import yaml
30
 
from fnmatch import fnmatch
31
 
 
32
 
import six
33
 
 
34
 
CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
35
 
 
36
 
 
37
 
def parse_config(conf_file):
38
 
    if not os.path.isfile(conf_file):
39
 
        logging.error('Invalid config file: %s.' % conf_file)
40
 
        return False
41
 
    return yaml.load(open(conf_file).read())
42
 
 
43
 
 
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)
49
 
    return dest
50
 
 
51
 
 
52
 
def _module_path(module):
53
 
    return os.path.join(*module.split('.'))
54
 
 
55
 
 
56
 
def _src_path(src, module):
57
 
    return os.path.join(src, 'charmhelpers', _module_path(module))
58
 
 
59
 
 
60
 
def _dest_path(dest, module):
61
 
    return os.path.join(dest, _module_path(module))
62
 
 
63
 
 
64
 
def _is_pyfile(path):
65
 
    return os.path.isfile(path + '.py')
66
 
 
67
 
 
68
 
def ensure_init(path):
69
 
    '''
70
 
    ensure directories leading up to path are importable, omitting
71
 
    parent directory, eg path='/hooks/helpers/foo'/:
72
 
        hooks/
73
 
        hooks/helpers/__init__.py
74
 
        hooks/helpers/foo/__init__.py
75
 
    '''
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()
81
 
 
82
 
 
83
 
def sync_pyfile(src, dest):
84
 
    src = src + '.py'
85
 
    src_dir = os.path.dirname(src)
86
 
    logging.info('Syncing pyfile: %s -> %s.' % (src, dest))
87
 
    if not os.path.exists(dest):
88
 
        os.makedirs(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'),
92
 
                    dest)
93
 
    ensure_init(dest)
94
 
 
95
 
 
96
 
def get_filter(opts=None):
97
 
    opts = opts or []
98
 
    if 'inc=*' in opts:
99
 
        # do not filter any files, include everything
100
 
        return None
101
 
 
102
 
    def _filter(dir, ls):
103
 
        incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt]
104
 
        _filter = []
105
 
        for f in ls:
106
 
            _f = os.path.join(dir, f)
107
 
 
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))
112
 
                    _filter.append(f)
113
 
                else:
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)
118
 
                _filter.append(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)
122
 
                _filter.append(f)
123
 
        return _filter
124
 
    return _filter
125
 
 
126
 
 
127
 
def sync_directory(src, dest, opts=None):
128
 
    if os.path.exists(dest):
129
 
        logging.debug('Removing existing directory: %s' % dest)
130
 
        shutil.rmtree(dest)
131
 
    logging.info('Syncing directory: %s -> %s.' % (src, dest))
132
 
 
133
 
    shutil.copytree(src, dest, ignore=get_filter(opts))
134
 
    ensure_init(dest)
135
 
 
136
 
 
137
 
def sync(src, dest, module, opts=None):
138
 
 
139
 
    # Sync charmhelpers/__init__.py for bootstrap code.
140
 
    sync_pyfile(_src_path(src, '__init__'), dest)
141
 
 
142
 
    # Sync other __init__.py files in the path leading to module.
143
 
    m = []
144
 
    steps = module.split('.')[:-1]
145
 
    while steps:
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)))
150
 
 
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)))
157
 
    else:
158
 
        logging.warn('Could not sync: %s. Neither a pyfile or directory, '
159
 
                     'does it even exist?' % module)
160
 
 
161
 
 
162
 
def parse_sync_options(options):
163
 
    if not options:
164
 
        return []
165
 
    return options.split(',')
166
 
 
167
 
 
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]
172
 
    if '|' not in inc:
173
 
        return (inc, global_options)
174
 
    inc, opts = inc.split('|')
175
 
    return (inc, parse_sync_options(opts) + global_options)
176
 
 
177
 
 
178
 
def sync_helpers(include, src, dest, options=None):
179
 
    if not os.path.isdir(dest):
180
 
        os.makedirs(dest)
181
 
 
182
 
    global_options = parse_sync_options(options)
183
 
 
184
 
    for inc in include:
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):
192
 
                    for m in v:
193
 
                        inc, opts = extract_options(m, global_options)
194
 
                        sync(src, dest, '%s.%s' % (k, inc), opts)
195
 
 
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()
207
 
 
208
 
    if opts.debug:
209
 
        logging.basicConfig(level=logging.DEBUG)
210
 
    else:
211
 
        logging.basicConfig(level=logging.INFO)
212
 
 
213
 
    if opts.config:
214
 
        logging.info('Loading charm helper config from %s.' % opts.config)
215
 
        config = parse_config(opts.config)
216
 
        if not config:
217
 
            logging.error('Could not parse config from %s.' % opts.config)
218
 
            sys.exit(1)
219
 
    else:
220
 
        config = {}
221
 
 
222
 
    if 'branch' not in config:
223
 
        config['branch'] = CHARM_HELPERS_BRANCH
224
 
    if opts.branch:
225
 
        config['branch'] = opts.branch
226
 
    if opts.dest_dir:
227
 
        config['destination'] = opts.dest_dir
228
 
 
229
 
    if 'destination' not in config:
230
 
        logging.error('No destination dir. specified as option or config.')
231
 
        sys.exit(1)
232
 
 
233
 
    if 'include' not in config:
234
 
        if not args:
235
 
            logging.error('No modules to sync specified as option or config.')
236
 
            sys.exit(1)
237
 
        config['include'] = []
238
 
        [config['include'].append(a) for a in args]
239
 
 
240
 
    sync_options = None
241
 
    if 'options' in config:
242
 
        sync_options = config['options']
243
 
    tmpd = tempfile.mkdtemp()
244
 
    try:
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)
250
 
        raise e
251
 
    finally:
252
 
        logging.debug('Cleaning up %s' % tmpd)
253
 
        shutil.rmtree(tmpd)