~andrewjbeach/juju-ci-tools/make-local-patcher

« back to all changes in this revision

Viewing changes to assess_resources_charmstore.py

  • Committer: Aaron Bentley
  • Date: 2015-06-15 19:04:10 UTC
  • mfrom: (976.2.4 fix-log-rotation)
  • Revision ID: aaron.bentley@canonical.com-20150615190410-vvhtl7yxn0xbtbiy
Fix error handling in assess_log_rotation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
 
3
 
from __future__ import print_function
4
 
 
5
 
import argparse
6
 
from collections import namedtuple
7
 
from datetime import datetime
8
 
import logging
9
 
import os
10
 
import sys
11
 
from tempfile import NamedTemporaryFile
12
 
from textwrap import dedent
13
 
from uuid import uuid1
14
 
 
15
 
import requests
16
 
 
17
 
from jujucharm import (
18
 
    CharmCommand,
19
 
    local_charm_path,
20
 
)
21
 
from utility import (
22
 
    configure_logging,
23
 
    JujuAssertionError,
24
 
    scoped_environ,
25
 
    temp_dir,
26
 
)
27
 
 
28
 
__metaclass__ = type
29
 
log = logging.getLogger("assess_resources_charmstore")
30
 
 
31
 
CHARMSTORE_API_VERSION = 'v5'
32
 
 
33
 
# Stores credential details for a target charmstore
34
 
CharmstoreDetails = namedtuple(
35
 
    'CharmstoreDetails',
36
 
    ['email', 'username', 'password', 'api_url'])
37
 
 
38
 
 
39
 
# Using a run id we can create a unique charm for each test run allowing us to
40
 
# test from fresh.
41
 
class RunId:
42
 
    _run_id = None
43
 
 
44
 
    def __call__(self):
45
 
        if self._run_id is None:
46
 
            self._run_id = str(uuid1()).replace('-', '')
47
 
        return self._run_id
48
 
 
49
 
 
50
 
get_run_id = RunId()
51
 
 
52
 
 
53
 
def get_charmstore_details(credentials_file=None):
54
 
    """Returns a CharmstoreDetails from `credentials_file` or env vars.
55
 
 
56
 
    Parses the credentials file (if supplied) and environment variables to
57
 
    retrieve the charmstore details and credentials.
58
 
 
59
 
    Note. any supplied detail via environment variables will overwrite anything
60
 
    supplied in the credentials file..
61
 
 
62
 
    """
63
 
 
64
 
    required_keys = ('api_url', 'password', 'email', 'username')
65
 
 
66
 
    details = {}
67
 
    if credentials_file is not None:
68
 
        details = parse_credentials_file(credentials_file)
69
 
 
70
 
    for key in required_keys:
71
 
        env_key = 'CS_{}'.format(key.upper())
72
 
        value = os.environ.get(env_key, details.get(key))
73
 
        # Can't have empty credential details
74
 
        if value is not None:
75
 
            details[key] = value
76
 
 
77
 
    if not set(details.keys()).issuperset(required_keys):
78
 
        raise ValueError('Unable to get all details from file.')
79
 
 
80
 
    return CharmstoreDetails(**details)
81
 
 
82
 
 
83
 
def split_line_details(string):
84
 
    safe_string = string.strip()
85
 
    return safe_string.split('=', 1)[-1].strip('"')
86
 
 
87
 
 
88
 
def parse_credentials_file(credentials_file):
89
 
    details = {}
90
 
    with open(credentials_file, 'r') as creds:
91
 
        for line in creds.readlines():
92
 
            if 'STORE_CREDENTIALS' in line:
93
 
                creds = split_line_details(line)
94
 
                email_address, password = creds.split(':', 1)
95
 
                details['email'] = email_address
96
 
                details['password'] = password
97
 
                raw_username = email_address.split('@', 1)[0]
98
 
                details['username'] = raw_username.replace('.', '-')
99
 
            elif 'STORE_URL' in line:
100
 
                details['api_url'] = split_line_details(line)
101
 
    return details
102
 
 
103
 
 
104
 
def ensure_can_push_and_list_charm_with_resources(charm_bin, cs_details):
105
 
    """Ensure that a charm can be pushed to a charm store with a resource.
106
 
 
107
 
    Checks that:
108
 
      - A charm can be pushed with a resource populated with a file
109
 
      - A charm can be updated (attach) after being pushed
110
 
      - A charms resources revision is updated after a push or attach
111
 
 
112
 
    """
113
 
    charm_command = CharmCommand(charm_bin, cs_details.api_url)
114
 
    with charm_command.logged_in_user(cs_details.email, cs_details.password):
115
 
        charm_id = 'juju-qa-resources-{id}'.format(id=get_run_id())
116
 
        # Only available for juju 2.x
117
 
        charm_path = local_charm_path('dummy-resource', '2.x')
118
 
        charm_url = 'cs:~{username}/{id}-0'.format(
119
 
            username=cs_details.username, id=charm_id)
120
 
 
121
 
        # Ensure we can publish a charm with a resource
122
 
        with NamedTemporaryFile(suffix='.txt') as temp_foo_resource:
123
 
            temp_foo = temp_foo_resource.name
124
 
            populate_file_data(temp_foo)
125
 
            push_charm_with_resource(
126
 
                charm_command,
127
 
                temp_foo,
128
 
                charm_id,
129
 
                charm_path,
130
 
                resource_name='foo')
131
 
 
132
 
            # Need to grant permissions so we can access details via the http
133
 
            # api.
134
 
            grant_everyone_access(charm_command, charm_url)
135
 
 
136
 
            expected_resource_details = {'foo': 0, 'bar': -1}
137
 
            check_resource_uploaded(
138
 
                charm_command,
139
 
                charm_url,
140
 
                'foo',
141
 
                temp_foo,
142
 
                expected_resource_details)
143
 
 
144
 
        # Ensure we can attach a resource independently of pushing a charm.
145
 
        with NamedTemporaryFile(suffix='.txt') as temp_bar_resource:
146
 
            temp_bar = temp_bar_resource.name
147
 
            populate_file_data(temp_bar)
148
 
            output = attach_resource_to_charm(
149
 
                charm_command, temp_bar, charm_url, resource_name='bar')
150
 
            log.info(output)
151
 
 
152
 
            expected_resource_details = {'foo': 0, 'bar': 0}
153
 
            check_resource_uploaded(
154
 
                charm_command,
155
 
                charm_url,
156
 
                'bar',
157
 
                temp_bar,
158
 
                expected_resource_details)
159
 
 
160
 
 
161
 
def populate_file_data(filepath):
162
 
    """Write unique data to file at `filepath`."""
163
 
    datestamp = datetime.utcnow().isoformat()
164
 
    with open(filepath, 'w') as f:
165
 
        f.write('{datestamp}:{uuid}'.format(
166
 
            datestamp=datestamp,
167
 
            uuid=get_run_id()))
168
 
 
169
 
 
170
 
def push_charm_with_resource(
171
 
        charm_command, temp_file, charm_id, charm_path, resource_name):
172
 
 
173
 
    output = charm_command.run(
174
 
        'push',
175
 
        charm_path,
176
 
        charm_id,
177
 
        '--resource', '{}={}'.format(resource_name, temp_file))
178
 
    log.info('Pushing charm "{id}": {output}'.format(
179
 
        id=charm_id, output=output))
180
 
 
181
 
 
182
 
def attach_resource_to_charm(
183
 
        charm_command, temp_file, charm_id, resource_name):
184
 
 
185
 
    return charm_command.run('attach', charm_id, '{}={}'.format(
186
 
        resource_name, temp_file))
187
 
 
188
 
 
189
 
def check_resource_uploaded(
190
 
        charm_command, charm_url, resource_name, src_file, resource_details):
191
 
    for check_name, check_revno in resource_details.items():
192
 
        check_resource_uploaded_revno(
193
 
            charm_command, charm_url, check_name, check_revno)
194
 
    check_resource_uploaded_contents(
195
 
        charm_command, charm_url, resource_name, src_file)
196
 
 
197
 
 
198
 
def check_resource_uploaded_revno(
199
 
        charm_command, charm_url, resource_name, revno):
200
 
    """Parse list-resources and ensure resource revno is equal to `revno`.
201
 
 
202
 
    :raises JujuAssertionError: If the resources revision is not equal to
203
 
      `revno`
204
 
 
205
 
    """
206
 
    revno = int(revno)
207
 
    output = charm_command.run('list-resources', charm_url)
208
 
 
209
 
    for line in output.split('\n'):
210
 
        if line.startswith(resource_name):
211
 
            rev = int(line.split(None, 1)[-1])
212
 
            if rev != revno:
213
 
                raise JujuAssertionError(
214
 
                    'Failed to upload resource and increment revision number.')
215
 
            return
216
 
    raise JujuAssertionError(
217
 
        'Failed to find named resource "{}" in output'.format(resource_name))
218
 
 
219
 
 
220
 
def grant_everyone_access(charm_command, charm_url):
221
 
    output = charm_command.run('grant', charm_url, 'everyone')
222
 
    log.info('Setting permissions on charm: {}'.format(output))
223
 
 
224
 
 
225
 
def check_resource_uploaded_contents(
226
 
        charm_command, charm_url, resource_name, src_file):
227
 
    charmname = charm_url.replace('cs:', '')
228
 
    api_url = '{apiurl}/{api_version}/{charmname}/resource/{name}'.format(
229
 
        apiurl=charm_command.api_url,
230
 
        api_version=CHARMSTORE_API_VERSION,
231
 
        charmname=charmname,
232
 
        name=resource_name,
233
 
    )
234
 
    log.info('Using api url: {}'.format(api_url))
235
 
 
236
 
    res = requests.get(api_url)
237
 
 
238
 
    if not res.ok:
239
 
        raise JujuAssertionError('Failed to retrieve details: {}'.format(
240
 
            res.content))
241
 
 
242
 
    with open(src_file, 'r') as f:
243
 
        file_contents = f.read()
244
 
    resource_contents = res.content
245
 
 
246
 
    raise_if_contents_differ(resource_contents, file_contents)
247
 
 
248
 
 
249
 
def raise_if_contents_differ(resource_contents, file_contents):
250
 
    if resource_contents != file_contents:
251
 
        raise JujuAssertionError(
252
 
            dedent("""\
253
 
            Resource contents mismatch.
254
 
            Expected:
255
 
            {}
256
 
            Got:
257
 
            {}""".format(
258
 
                file_contents,
259
 
                resource_contents)))
260
 
 
261
 
 
262
 
def assess_charmstore_resources(charm_bin, credentials_file):
263
 
    with temp_dir() as fake_home:
264
 
        temp_env = os.environ.copy()
265
 
        temp_env['HOME'] = fake_home
266
 
        with scoped_environ(temp_env):
267
 
            cs_details = get_charmstore_details(credentials_file)
268
 
            ensure_can_push_and_list_charm_with_resources(
269
 
                charm_bin, cs_details)
270
 
 
271
 
 
272
 
def parse_args(argv):
273
 
    """Parse all arguments."""
274
 
    parser = argparse.ArgumentParser(description="Assess resources charmstore")
275
 
    parser.add_argument('charm_bin', help='Full path to charn binary')
276
 
    parser.add_argument(
277
 
        'credentials_file',
278
 
        help='Path to the file containing the charm store credentials and url')
279
 
    parser.add_argument(
280
 
        '--verbose', action='store_const',
281
 
        default=logging.INFO, const=logging.DEBUG,
282
 
        help='Verbose test harness output.')
283
 
    return parser.parse_args(argv)
284
 
 
285
 
 
286
 
def check_resources():
287
 
    if os.environ.get('JUJU_REPOSITORY') is None:
288
 
        raise AssertionError('JUJU_REPOSITORY required')
289
 
 
290
 
 
291
 
def main(argv=None):
292
 
    args = parse_args(argv)
293
 
    configure_logging(args.verbose)
294
 
 
295
 
    check_resources()
296
 
 
297
 
    assess_charmstore_resources(args.charm_bin, args.credentials_file)
298
 
    return 0
299
 
 
300
 
 
301
 
if __name__ == '__main__':
302
 
    sys.exit(main())