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)