github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/jujucharm.py (about) 1 """Helpers to create and manage local juju charms.""" 2 3 from contextlib import contextmanager 4 import logging 5 import os 6 import pexpect 7 import re 8 import subprocess 9 10 import yaml 11 12 from utility import ( 13 ensure_deleted, 14 JujuAssertionError, 15 ) 16 17 18 __metaclass__ = type 19 20 21 log = logging.getLogger("jujucharm") 22 23 24 class Charm: 25 """Representation of a juju charm.""" 26 27 DEFAULT_MAINTAINER = "juju-qa@lists.canonical.com" 28 DEFAULT_SERIES = ("bionic", "xenial", "trusty") 29 DEFAULT_DESCRIPTION = "description" 30 31 NAME_REGEX = re.compile('^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$') 32 33 def __init__(self, name, summary, maintainer=None, series=None, 34 description=None, storage=None, ensure_valid_name=True): 35 if ensure_valid_name and Charm.NAME_REGEX.match(name) is None: 36 raise JujuAssertionError( 37 'Invalid Juju Charm Name, "{}" does not match "{}".'.format( 38 name, Charm.NAME_REGEX.pattern)) 39 self.metadata = { 40 "name": name, 41 "summary": summary, 42 "maintainer": maintainer or self.DEFAULT_MAINTAINER, 43 "series": series or self.DEFAULT_SERIES, 44 "description": description or self.DEFAULT_DESCRIPTION 45 } 46 if storage is not None: 47 self.metadata["storage"] = storage 48 self._hook_scripts = {} 49 50 def to_dir(self, directory): 51 """Serialize charm into a new directory.""" 52 with open(os.path.join(directory, "metadata.yaml"), "w") as f: 53 yaml.safe_dump(self.metadata, f, default_flow_style=False) 54 if self._hook_scripts: 55 hookdir = os.path.join(directory, "hooks") 56 os.mkdir(hookdir) 57 for hookname in self._hook_scripts: 58 with open(os.path.join(hookdir, hookname), "w") as f: 59 os.fchmod(f.fileno(), 0o755) 60 f.write(self._hook_scripts[hookname]) 61 62 def to_repo_dir(self, repo_dir): 63 """Serialize charm into a directory for a repository of charms.""" 64 charm_dir = os.path.join( 65 repo_dir, self.default_series, self.metadata["name"]) 66 os.makedirs(charm_dir) 67 self.to_dir(charm_dir) 68 return charm_dir 69 70 @property 71 def default_series(self): 72 series = self.metadata.get("series", self.DEFAULT_SERIES) 73 if series and isinstance(series, (tuple, list)): 74 return series[0] 75 return series 76 77 def add_hook_script(self, name, script): 78 self._hook_scripts[name] = script 79 80 81 def local_charm_path(charm, juju_ver, series=None, repository=None, 82 platform='ubuntu'): 83 """Create either Juju 1.x or 2.x local charm path.""" 84 if juju_ver.startswith('1.'): 85 if series: 86 series = '{}/'.format(series) 87 else: 88 series = '' 89 local_path = 'local:{}{}'.format(series, charm) 90 return local_path 91 else: 92 charm_dir = { 93 'ubuntu': 'charms', 94 'win': 'charms-win', 95 'centos': 'charms-centos'} 96 abs_path = charm 97 if repository: 98 abs_path = os.path.join(repository, charm) 99 elif os.environ.get('JUJU_REPOSITORY'): 100 repository = os.path.join( 101 os.environ['JUJU_REPOSITORY'], charm_dir[platform]) 102 abs_path = os.path.join(repository, charm) 103 return abs_path 104 105 106 class CharmCommand: 107 default_api_url = 'https://api.jujucharms.com/charmstore' 108 109 def __init__(self, charm_bin, api_url=None): 110 """Simple charm command wrapper.""" 111 self.charm_bin = charm_bin 112 self.api_url = sane_charm_store_api_url(api_url) 113 114 def _get_env(self): 115 return {'JUJU_CHARMSTORE': self.api_url} 116 117 @contextmanager 118 def logged_in_user(self, user_email, password): 119 """Contextmanager that logs in and ensures user logs out.""" 120 try: 121 self.login(user_email, password) 122 yield 123 finally: 124 try: 125 self.logout() 126 except Exception as e: 127 log.error('Failed to logout: {}'.format(str(e))) 128 default_juju_data = os.path.join( 129 os.environ['HOME'], '.local', 'share', 'juju') 130 juju_data = os.environ.get('JUJU_DATA', default_juju_data) 131 token_file = os.path.join(juju_data, 'store-usso-token') 132 cookie_file = os.path.join(os.environ['HOME'], '.go-cookies') 133 log.debug('Removing {} and {}'.format(token_file, cookie_file)) 134 ensure_deleted(token_file) 135 ensure_deleted(cookie_file) 136 137 def login(self, user_email, password): 138 log.debug('Logging {} in.'.format(user_email)) 139 try: 140 command = pexpect.spawn( 141 self.charm_bin, ['login'], env=self._get_env()) 142 command.expect('(?i)Login to Ubuntu SSO') 143 command.expect('(?i)Press return to select.*\.') 144 command.expect('(?i)E-Mail:') 145 command.sendline(user_email) 146 command.expect('(?i)Password') 147 command.sendline(password) 148 command.expect('(?i)Two-factor auth') 149 command.sendline() 150 command.expect(pexpect.EOF) 151 if command.isalive(): 152 raise AssertionError( 153 'Failed to log user in to {}'.format( 154 self.api_url)) 155 except (pexpect.TIMEOUT, pexpect.EOF) as e: 156 raise AssertionError( 157 'Failed to log user in: {}'.format(e)) 158 159 def logout(self): 160 log.debug('Logging out.') 161 self.run('logout') 162 163 def run(self, sub_command, *arguments): 164 try: 165 output = subprocess.check_output( 166 [self.charm_bin, sub_command] + list(arguments), 167 env=self._get_env(), 168 stderr=subprocess.STDOUT) 169 return output 170 except subprocess.CalledProcessError as e: 171 log.error(e.output) 172 raise 173 174 175 def sane_charm_store_api_url(url): 176 """Ensure the store url includes the right parts.""" 177 if url is None: 178 return CharmCommand.default_api_url 179 return '{}/charmstore'.format(url)