github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/assess_wallet.py (about) 1 #!/usr/bin/env python 2 """ 3 This tests the wallet commands utilized for commercial charm billing. 4 These commands are linked to a ubuntu sso account, and as such, require the 5 user account to be setup before test execution (including authentication). 6 You can use charm login to do this, or let juju authenticate with a browser. 7 """ 8 9 from __future__ import print_function 10 11 import argparse 12 from fixtures import EnvironmentVariable 13 import json 14 import logging 15 import os 16 import pexpect 17 from random import randint 18 import shutil 19 import subprocess 20 import sys 21 22 23 from deploy_stack import ( 24 BootstrapManager, 25 ) 26 27 from utility import ( 28 add_basic_testing_arguments, 29 temp_dir, 30 JujuAssertionError, 31 configure_logging, 32 ) 33 34 __metaclass__ = type 35 36 37 log = logging.getLogger("assess_wallet") 38 39 40 def _get_new_wallet_limit(client): 41 """Return available limit for new wallet""" 42 wallets = json.loads(list_wallets(client)) 43 limit = int(wallets['total']['limit']) 44 credit = int(wallets['credit']) 45 log.debug('Found credit limit {}, currently used {}'.format( 46 credit, limit)) 47 return credit - limit 48 49 50 def _get_wallets(client): 51 return json.loads(list_wallets(client))['wallets'] 52 53 54 def _set_wallet_value_expectations(expected_wallets, name, value): 55 # Update our expectations accordingly 56 for wallet in expected_wallets: 57 if wallet['wallet'] == name: 58 # For now, we assume we aren't spending down the wallet 59 wallet['limit'] = value 60 wallet['unallocated'] = value 61 # .00 is appended to availible for some reason 62 wallet['available'] = '{:.2f}'.format(float(value)) 63 log.info('Expected wallet updated: "{}" to {}'.format(name, value)) 64 65 66 def _try_setting_wallet(client, name, value): 67 try: 68 output = set_wallet(client, name, value) 69 except subprocess.CalledProcessError as e: 70 output = [e.output, getattr(e, 'stderr', '')] 71 raise JujuAssertionError('Could not set wallet {}'.format(output)) 72 73 if 'wallet limit updated' not in output: 74 raise JujuAssertionError('Error calling set-wallet {}'.format(output)) 75 76 77 def _try_creating_wallet(client, name, value): 78 try: 79 create_wallet(client, name, value) 80 log.info('Created new wallet "{}" with value {}'.format(name, 81 value)) 82 except subprocess.CalledProcessError as e: 83 output = [e.output, getattr(e, 'stderr', '')] 84 if any('already exists' in message for message in output): 85 log.info('Reusing wallet "{}" with value {}'.format(name, value)) 86 pass # this will be a failure once lp:1663258 is fixed 87 else: 88 raise JujuAssertionError( 89 'Error testing create-wallet: {}'.format(output)) 90 except: 91 raise JujuAssertionError('Added duplicate wallet') 92 93 94 def _try_greater_than_limit_wallet(client, name, limit): 95 error_strings = { 96 'pass': 'exceed the credit limit', 97 'unknown': 'Error testing wallet greater than credit limit', 98 'fail': 'Credit limit exceeded' 99 } 100 over_limit_value = str(limit + randint(1, 100)) 101 assert_set_wallet(client, name, over_limit_value, error_strings) 102 103 104 def _try_negative_wallet(client, name): 105 error_strings = { 106 'pass': 'Could not set wallet', 107 'unknown': 'Error testing negative wallet', 108 'fail': 'Negative wallet allowed' 109 } 110 negative_wallet_value = str(randint(-1000, -1)) 111 assert_set_wallet(client, name, negative_wallet_value, error_strings) 112 113 114 def assert_sorted_equal(found, expected): 115 found = sorted(found) 116 expected = sorted(expected) 117 if found != expected: 118 raise JujuAssertionError( 119 'Found: {}\nExpected: {}'.format(found, expected)) 120 121 122 def assert_set_wallet(client, name, limit, error_strings): 123 try: 124 _try_setting_wallet(client, name, limit) 125 except JujuAssertionError as e: 126 if error_strings['pass'] not in e.message: 127 raise JujuAssertionError( 128 '{}: {}'.format(error_strings['unknown'], e)) 129 else: 130 raise JujuAssertionError(error_strings['fail']) 131 132 133 def create_wallet(client, name, value): 134 """Create a wallet""" 135 return client.get_juju_output('create-wallet', name, value, 136 include_e=False) 137 138 139 def list_wallets(client): 140 """Return defined wallets as json.""" 141 return client.get_juju_output('list-wallets', '--format', 'json', 142 include_e=False) 143 144 145 def set_wallet(client, name, value): 146 """Change an existing wallet's allocation.""" 147 return client.get_juju_output('set-wallet', name, value, include_e=False) 148 149 150 def show_wallet(client, name): 151 """Return specified wallet as json.""" 152 return client.get_juju_output('show-wallet', name, '--format', 'json', 153 include_e=False) 154 155 156 def assess_wallet(client): 157 # Since we can't remove wallets until lp:1663258 158 # is fixed, we avoid creating new random wallets and hardcode. 159 # We also, zero out the previous wallet 160 wallet_name = 'personal' 161 _try_setting_wallet(client, wallet_name, '0') 162 163 wallet_limit = _get_new_wallet_limit(client) 164 assess_wallet_limit(wallet_limit) 165 166 expected_wallets = _get_wallets(client) 167 wallet_value = str(randint(1, wallet_limit / 2)) 168 assess_create_wallet(client, wallet_name, wallet_value, wallet_limit) 169 170 wallet_value = str(randint(wallet_limit / 2 + 1, wallet_limit)) 171 assess_set_wallet(client, wallet_name, wallet_value, wallet_limit) 172 assess_show_wallet(client, wallet_name, wallet_value) 173 174 _set_wallet_value_expectations(expected_wallets, wallet_name, wallet_value) 175 assess_list_wallets(client, expected_wallets) 176 177 178 def assess_wallet_limit(wallet_limit): 179 log.info('Assessing wallet limit {}'.format(wallet_limit)) 180 181 if wallet_limit < 0: 182 raise JujuAssertionError( 183 'Negative Wallet Limit {}'.format(wallet_limit)) 184 185 186 def assess_create_wallet(client, wallet_name, wallet_value, wallet_limit): 187 """Test create-wallet command""" 188 log.info('create-wallet "{}" with value {}, limit {}'.format(wallet_name, 189 wallet_value, 190 wallet_limit)) 191 192 # Do this twice, to ensure wallet exists and we can check for 193 # duplicate message. Ideally, once lp:1663258 is fixed, we will 194 # assert on initial wallet creation as well. 195 _try_creating_wallet(client, wallet_name, wallet_value) 196 197 log.info('Trying duplicate create-wallet') 198 _try_creating_wallet(client, wallet_name, wallet_value) 199 200 201 def assess_list_wallets(client, expected_wallets): 202 log.info('list-wallets testing expected values') 203 # Since we can't remove wallets until lp:1663258 204 # is fixed, we don't modify the list contents or count 205 # Nonetheless, we assert on it for future use 206 wallets = _get_wallets(client) 207 assert_sorted_equal(wallets, expected_wallets) 208 209 210 def assess_set_wallet(client, wallet_name, wallet_value, wallet_limit): 211 """Test set-wallet command""" 212 log.info('set-wallet "{}" with value {}, limit {}'.format(wallet_name, 213 wallet_value, 214 wallet_limit)) 215 _try_setting_wallet(client, wallet_name, wallet_value) 216 217 # Check some bounds 218 # Since walletting is important, and the functional test is cheap, 219 # let's test some basic bounds 220 log.info('Trying set-wallet with value greater than wallet limit') 221 _try_greater_than_limit_wallet(client, wallet_name, wallet_limit) 222 223 log.info('Trying set-wallet with negative value') 224 _try_negative_wallet(client, wallet_name) 225 226 227 def assess_show_wallet(client, wallet_name, wallet_value): 228 log.info('show-wallet "{}" with value {}'.format(wallet_name, 229 wallet_value)) 230 231 wallet = json.loads(show_wallet(client, wallet_name)) 232 233 # assert wallet value 234 if wallet['limit'] != wallet_value: 235 raise JujuAssertionError('Wallet limit found {}, expected {}'.format( 236 wallet['limit'], wallet_value)) 237 238 # assert on usage (0% until we use it) 239 if wallet['total']['usage'] != '0%': 240 raise JujuAssertionError('Wallet usage found {}, expected {}'.format( 241 wallet['total']['usage'], '0%')) 242 243 244 def parse_args(argv): 245 """Parse all arguments.""" 246 parser = argparse.ArgumentParser(description="Test wallet commands") 247 # Set to false it it's possible to overwrite actual cookie data if someone 248 # runs it against an existing environment 249 add_basic_testing_arguments(parser, existing=False) 250 return parser.parse_args(argv) 251 252 253 def set_controller_cookie_file(client): 254 """Plant pre-generated cookie file to avoid launching Browser. 255 256 Using an existing usso token use 'charm login' to create a .go-cookies file 257 (in a tmp HOME). Copy this new cookies file to become the controller cookie 258 file. 259 """ 260 261 with temp_dir() as tmp_home: 262 with EnvironmentVariable('HOME', tmp_home): 263 move_usso_token_to_juju_home(tmp_home) 264 265 # charm login shouldn't be interactive, fail if it is. 266 try: 267 command = pexpect.spawn( 268 'charm', ['login'], env={'HOME': tmp_home}) 269 command.expect(pexpect.EOF) 270 except (pexpect).TIMEOUT: 271 raise RuntimeError('charm login command was interactive.') 272 273 go_cookie = os.path.join(tmp_home, '.go-cookies') 274 controller_cookie_path = os.path.join( 275 client.env.juju_home, 276 'cookies', 277 '{}.json'.format(client.env.controller.name)) 278 279 shutil.copyfile(go_cookie, controller_cookie_path) 280 281 282 def move_usso_token_to_juju_home(tmp_home): 283 """Move pre-packaged token to juju data dir. 284 285 Move the stored store-usso-token to a tmp juju home dir for charm command 286 use. 287 """ 288 source_usso_path = os.path.join( 289 os.environ['JUJU_HOME'], 'juju-bot-store-usso-token') 290 dest_usso_dir = os.path.join(tmp_home, '.local', 'share', 'juju') 291 os.makedirs(dest_usso_dir) 292 dest_usso_path = os.path.join(dest_usso_dir, 'store-usso-token') 293 shutil.copyfile(source_usso_path, dest_usso_path) 294 295 296 def main(argv=None): 297 args = parse_args(argv) 298 configure_logging(args.verbose) 299 bs_manager = BootstrapManager.from_args(args) 300 with bs_manager.booted_context(args.upload_tools): 301 set_controller_cookie_file(bs_manager.client) 302 assess_wallet(bs_manager.client) 303 return 0 304 305 306 if __name__ == '__main__': 307 sys.exit(main())