3
from __future__ import print_function
6
from collections import namedtuple
7
from datetime import datetime
11
from tempfile import NamedTemporaryFile
12
from textwrap import dedent
13
from uuid import uuid1
17
from jujucharm import (
29
log = logging.getLogger("assess_resources_charmstore")
31
CHARMSTORE_API_VERSION = 'v5'
33
# Stores credential details for a target charmstore
34
CharmstoreDetails = namedtuple(
36
['email', 'username', 'password', 'api_url'])
39
# Using a run id we can create a unique charm for each test run allowing us to
45
if self._run_id is None:
46
self._run_id = str(uuid1()).replace('-', '')
53
def get_charmstore_details(credentials_file=None):
54
"""Returns a CharmstoreDetails from `credentials_file` or env vars.
56
Parses the credentials file (if supplied) and environment variables to
57
retrieve the charmstore details and credentials.
59
Note. any supplied detail via environment variables will overwrite anything
60
supplied in the credentials file..
64
required_keys = ('api_url', 'password', 'email', 'username')
67
if credentials_file is not None:
68
details = parse_credentials_file(credentials_file)
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
77
if not set(details.keys()).issuperset(required_keys):
78
raise ValueError('Unable to get all details from file.')
80
return CharmstoreDetails(**details)
83
def split_line_details(string):
84
safe_string = string.strip()
85
return safe_string.split('=', 1)[-1].strip('"')
88
def parse_credentials_file(credentials_file):
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)
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.
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
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)
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(
132
# Need to grant permissions so we can access details via the http
134
grant_everyone_access(charm_command, charm_url)
136
expected_resource_details = {'foo': 0, 'bar': -1}
137
check_resource_uploaded(
142
expected_resource_details)
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')
152
expected_resource_details = {'foo': 0, 'bar': 0}
153
check_resource_uploaded(
158
expected_resource_details)
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(
170
def push_charm_with_resource(
171
charm_command, temp_file, charm_id, charm_path, resource_name):
173
output = charm_command.run(
177
'--resource', '{}={}'.format(resource_name, temp_file))
178
log.info('Pushing charm "{id}": {output}'.format(
179
id=charm_id, output=output))
182
def attach_resource_to_charm(
183
charm_command, temp_file, charm_id, resource_name):
185
return charm_command.run('attach', charm_id, '{}={}'.format(
186
resource_name, temp_file))
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)
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`.
202
:raises JujuAssertionError: If the resources revision is not equal to
207
output = charm_command.run('list-resources', charm_url)
209
for line in output.split('\n'):
210
if line.startswith(resource_name):
211
rev = int(line.split(None, 1)[-1])
213
raise JujuAssertionError(
214
'Failed to upload resource and increment revision number.')
216
raise JujuAssertionError(
217
'Failed to find named resource "{}" in output'.format(resource_name))
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))
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,
234
log.info('Using api url: {}'.format(api_url))
236
res = requests.get(api_url)
239
raise JujuAssertionError('Failed to retrieve details: {}'.format(
242
with open(src_file, 'r') as f:
243
file_contents = f.read()
244
resource_contents = res.content
246
raise_if_contents_differ(resource_contents, file_contents)
249
def raise_if_contents_differ(resource_contents, file_contents):
250
if resource_contents != file_contents:
251
raise JujuAssertionError(
253
Resource contents mismatch.
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)
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')
278
help='Path to the file containing the charm store credentials and url')
280
'--verbose', action='store_const',
281
default=logging.INFO, const=logging.DEBUG,
282
help='Verbose test harness output.')
283
return parser.parse_args(argv)
286
def check_resources():
287
if os.environ.get('JUJU_REPOSITORY') is None:
288
raise AssertionError('JUJU_REPOSITORY required')
292
args = parse_args(argv)
293
configure_logging(args.verbose)
297
assess_charmstore_resources(args.charm_bin, args.credentials_file)
301
if __name__ == '__main__':