Пример #1
0
 def __init__(self, **params):
     super().__init__(**params)
     self._session = AEUserSession(self.hostname,
                                   self.username,
                                   self.password,
                                   persist=False,
                                   k8s_endpoint=self.k8s_endpoint)
     adapter = requests.adapters.HTTPAdapter(pool_maxsize=self.pool_size)
     self._session.session.mount('https://', adapter)
     self._session.session.mount('http://', adapter)
Пример #2
0
def test_login_time(admin_session, user_session):
    # The current session should already be authenticated
    now = datetime.utcnow()
    plist0 = user_session.project_list()
    user_list = admin_session.user_list()
    urec = next(
        (r for r in user_list if r['username'] == user_session.username), None)
    assert urec is not None
    ltm1 = datetime.fromtimestamp(urec['lastLogin'] / 1000.0)
    assert ltm1 < now

    # Create new login session. This should change lastLogin
    password = os.environ.get('AE5_PASSWORD')
    user_sess2 = AEUserSession(user_session.hostname,
                               user_session.username,
                               password,
                               persist=False)
    plist1 = user_sess2.project_list()
    urec = admin_session.user_info(urec['id'])
    ltm2 = datetime.fromtimestamp(urec['lastLogin'] / 1000.0)
    assert ltm2 > ltm1
    user_sess2.disconnect()
    assert plist1 == plist0

    # Create new impersonation session. This should not change lastLogin
    user_sess3 = AEUserSession(admin_session.hostname,
                               user_session.username,
                               admin_session,
                               persist=False)
    plist2 = user_sess3.project_list()
    urec = admin_session.user_info(urec['id'])
    ltm3 = datetime.fromtimestamp(urec['lastLogin'] / 1000.0)
    assert ltm3 == ltm2
    user_sess3.disconnect()
    # Confirm the impersonation worked by checking the project lists are the same
    assert plist2 == plist0

    # Access the original login session. It should not reauthenticate
    plist3 = user_session.project_list()
    urec = admin_session.user_info(urec['id'])
    ltm4 = datetime.fromtimestamp(urec['lastLogin'] / 1000.0)
    assert ltm4 == ltm3
    assert plist3 == plist0
Пример #3
0
def user_session():
    hostname, username, password = _get_vars('AE5_HOSTNAME', 'AE5_USERNAME',
                                             'AE5_PASSWORD')
    return AEUserSession(hostname, username, password)
Пример #4
0
class AE5Source(Source):
    """
    The AE5Source provides a number of tables, which allow monitoring
    the nodes, deployments, sessions and resource profiles of an
    Anaconda Enterprise 5 installation.
    """

    hostname = param.String(doc="URL of the AE5 host.")

    username = param.String(doc="Username to authenticate with AE5.")

    password = param.String(doc="Password to authenticate with AE5.")

    k8s_endpoint = param.String(default='k8s')

    pool_size = param.Integer(default=100,
                              doc="""
      Size of HTTP socket pool.""")

    private = param.Boolean(default=True,
                            doc="""
      Whether to limit the deployments visible to a user based on
      their authorization.""")

    source_type = 'ae5'

    _deployment_columns = [
        'id', 'name', 'url', 'owner', 'resource_profile', 'public', 'state',
        'cpu', 'cpu_percent', 'memory', 'memory_percent', 'uptime', 'restarts'
    ]

    _session_columns = [
        'id', 'name', 'url', 'owner', 'resource_profile', 'state'
    ]

    _tables = ['deployments', 'nodes', 'resources', 'sessions']

    _units = {
        'm': 0.001,
        None: 1,
        'Ki': 1024,
        'Mi': 1024**2,
        'Gi': 1024**3,
        'Ti': 1024**4
    }

    def __init__(self, **params):
        super().__init__(**params)
        self._session = AEUserSession(self.hostname,
                                      self.username,
                                      self.password,
                                      persist=False,
                                      k8s_endpoint=self.k8s_endpoint)
        adapter = requests.adapters.HTTPAdapter(pool_maxsize=self.pool_size)
        self._session.session.mount('https://', adapter)
        self._session.session.mount('http://', adapter)

    @classmethod
    def _convert_value(cls, value):
        if value == '0':
            return 0
        val_unit = [
            m for m in cls._units if m is not None and value.endswith(m)
        ]
        val_unit = val_unit[0] if val_unit else None
        if not val_unit:
            return float(value)
        scale_factor = cls._units[val_unit]
        return float(value[:-len(val_unit)]) * scale_factor

    @property
    def _user(self):
        return state.headers.get('Anaconda-User') if self.private else None

    def _process_deployment(self, deployment):
        container_info = deployment.get('_k8s', {}).get('containers', {})
        if 'app' not in container_info:
            nan = float('NaN')
            deployment['cpu'] = nan
            deployment['memory'] = nan
            deployment['restarts'] = nan
            deployment['uptime'] = '-'
            return deployment

        container = container_info['app']
        limits = container['limits']
        usage = container['usage']

        # CPU Usage
        cpu_cap = self._convert_value(limits['cpu'])
        if 'cpu' in usage:
            cpu = self._convert_value(usage['cpu'])
            deployment['cpu'] = cpu
            deployment['cpu_percent'] = round((cpu / cpu_cap) * 100, 2)
        else:
            deployment['cpu_percent'] = float('nan')
            deployment['cpu'] = float('nan')

        # Memory usage
        mem_cap = self._convert_value(limits['memory'])
        if 'memory' in usage:
            mem = self._convert_value(usage['memory'])
            deployment['memory'] = mem
            deployment['memory_percent'] = round((mem / mem_cap) * 100, 2)
        else:
            deployment['memory'] = float('nan')
            deployment['memory_percent'] = float('nan')

        # Uptime
        started = container.get('since')
        deployment["restarts"] = container['restarts']
        if started:
            started_dt = dt.datetime.fromisoformat(started.replace('Z', ''))
            uptime = str(dt.datetime.now() - started_dt)
            deployment["uptime"] = uptime[:uptime.index('.')]
        else:
            deployment["uptime"] = '-'

        return deployment

    def _process_nodes(self, node):
        caps = {
            'cpu': self._convert_value(node['capacity/cpu']),
            'gpu': self._convert_value(node['capacity/gpu']),
            'mem': self._convert_value(node['capacity/mem']),
            'pod': int(node['capacity/pod'])
        }
        for column in node.index:
            if 'capacity' in column or '/' not in column:
                continue
            vtype = column.split('/')[1]
            cap = caps[vtype]
            value = node[column]
            if vtype != 'pod':
                value = self._convert_value(value)
            node[column] = value
            node[f'{column}_percent'] = 0 if cap == 0 else round(
                (value / caps[vtype]) * 100, 2)
        return node

    def _get_deployments(self):
        user = self._user
        deployments = self._session.deployment_list(
            k8s=True, format='dataframe',
            collaborators=bool(user)).apply(self._process_deployment, axis=1)
        if user is None:
            return deployments[self._deployment_columns]
        return deployments[deployments.public | (deployments.owner == user)
                           | deployments._collaborators.apply(
                               lambda x: user in x)][self._deployment_columns]

    def _get_nodes(self):
        nodes = self._session.node_list(format='dataframe').apply(
            self._process_nodes, axis=1)
        return nodes[[c for c in nodes.columns if not c.startswith('_')]]

    def _get_resources(self):
        return self._session.resource_profile_list(format='dataframe')

    def _get_sessions(self):
        sessions = self._session.session_list(format='dataframe')
        if self._user:
            sessions = sessions[sessions.owner == self._user]
        return sessions[self._session_columns]

    @cached(with_query=False)
    def get(self, table, **query):
        if table not in self._tables:
            raise ValueError(
                f"AE5Source has no '{table}' table, choose from {repr(self._tables)}."
            )
        return getattr(self, f'_get_{table}')()

    def get_schema(self, table=None):
        schemas = {}
        for t in self._tables:
            if table is None or t == table:
                schemas[t] = get_dataframe_schema(
                    self.get(t))['items']['properties']
        return schemas if table is None else schemas[table]
Пример #5
0
def user_session():
    hostname, username, password = _get_vars('AE5_HOSTNAME', 'AE5_USERNAME', 'AE5_PASSWORD')
    s = AEUserSession(hostname, username, password)
    for run in s.run_list():
        s.run_delete(run)
    for job in s.job_list():
        s.job_delete(job)
    for dep in s.deployment_list():
        s.deployment_stop(dep)
    for sess in s.session_list():
        s.session_stop(sess)
    plist = s.project_list()
    for p in plist:
        if p['name'] not in {'testproj1', 'testproj2', 'testproj3'} and p['owner'] == username:
            s.project_delete(p['id'])
    # Make sure testproj3 is using the Jupyter editor
    prec = s.project_info(f'{username}/testproj3', collaborators=True)
    if prec['editor'] != 'notebook' or prec['resource_profile'] != 'default':
        s.project_patch(prec, editor='notebook', resource_profile='default')
    # Make sure testproj3 has no collaborators
    if prec['_collaborators']:
        collabs = tuple(c['id'] for c in prec['_collaborators'])
        s.project_collaborator_remove(prec, collabs) 
        plist = s.project_list(collaborators=True)
    plist = s.project_list(collaborators=True)
    powned = [p for p in plist if p['owner'] == username]
    pother = [p for p in plist if p['owner'] != username]
    # Assert there are exactly 3 projects owned by the test user
    assert len(powned) == 3
    # Need at least two duplicated project names to properly test sorting/filtering
    assert len(set(p['name'] for p in powned).intersection(p['name'] for p in pother)) >= 2
    # Make sure all three editors are represented
    assert len(set(p['editor'] for p in powned)) == 3
    # Make sure we have 0, 1, and 2 collaborators represented
    assert set(len(p['_collaborators']) for p in plist
               if p['owner'] == username).issuperset((0, 1, 2))
    yield s
    s.disconnect()
Пример #6
0
def test_user_session(monkeypatch, capsys):
    with pytest.raises(ValueError) as excinfo:
        AEUserSession('', '')
    assert 'Must supply hostname and username' in str(excinfo.value)
    hostname, username, password = _get_vars('AE5_HOSTNAME', 'AE5_USERNAME',
                                             'AE5_PASSWORD')
    with pytest.raises(AEException) as excinfo:
        c = AEUserSession(hostname, username, 'x' + password, persist=False)
        c.authorize()
        del c
    assert 'Invalid username or password.' in str(excinfo.value)
    passwords = [password, '', 'x' + password]
    monkeypatch.setattr('getpass.getpass', lambda x: passwords.pop())
    c = AEUserSession(hostname, username, persist=False)
    c.authorize()
    captured = capsys.readouterr()
    assert f'Password for {username}@{hostname}' in captured.err
    assert f'Invalid username or password; please try again.' in captured.err
    assert f'Must supply a password' in captured.err
    true_endpoint, c._k8s_endpoint = c._k8s_endpoint, 'ssh:fakeuser'
    with pytest.raises(AEException) as excinfo:
        c._k8s('status')
    assert 'Error establishing k8s connection' in str(excinfo.value)
    c._k8s_endpoint = 'fakek8sendpoint'
    with pytest.raises(AEException) as excinfo:
        c._k8s('status')
    assert 'No deployment found at endpoint fakek8sendpoint' in str(
        excinfo.value)
    with pytest.raises(AEException) as excinfo:
        c._k8s('status')
    assert 'No k8s connection available' in str(excinfo.value)
    c._k8s_endpoint = true_endpoint
    assert c._k8s('status') == 'Alive and kicking'
Пример #7
0
def user_setup():
    hostname, username, password = _get_vars('AE5_HOSTNAME', 'AE5_USERNAME',
                                             'AE5_PASSWORD')
    s = AEUserSession(hostname, username, password)
    for run in s.run_list():
        s.run_delete(run['id'])
    for job in s.job_list():
        s.job_delete(job['id'])
    for dep in s.deployment_list():
        s.deployment_stop(dep['id'])
    for sess in s.session_list():
        s.session_stop(sess['id'])
    plist = s.project_list(collaborators=True)
    for p in plist:
        if p['name'] not in {'testproj1', 'testproj2', 'testproj3'
                             } and p['owner'] == username:
            s.project_delete(p['id'])
    plist = s.project_list(collaborators=True)
    powned = [p for p in plist if p['owner'] == username]
    pother = [p for p in plist if p['owner'] != username]
    # Assert there are exactly 3 projects owned by the test user
    assert len(powned) == 3
    # Need at least two duplicated project names to properly test sorting/filtering
    assert len(
        set(p['name'] for p in powned).intersection(p['name']
                                                    for p in pother)) >= 2
    # Make sure all three editors are represented
    assert len(set(p['editor'] for p in powned)) == 3
    # Make sure we have 0, 1, and 2 collaborators represented
    assert set(
        len(p['collaborators'].split(', ')) if p['collaborators'] else 0
        for p in powned).issuperset((0, 1, 2))
    yield s, plist
    s.disconnect()