Skip to content

Commit

Permalink
Merge pull request #15 from icgood/config
Browse files Browse the repository at this point in the history
Save connection info to a config file
  • Loading branch information
icgood committed Nov 12, 2020
2 parents a272b15 + 7ba9be7 commit 5d1b19a
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 106 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ See the `pymap-admin --help` commands for other connection options.

## Commands

### `save-args` Command

When administering remote pymap servers, it can be cumbersome to always supply
connection arguments every time, such as `--host`. This command saves the
arguments it is given to a config file.

```console
$ pymap-admin --host imap.example.com --port 50051 save-args
Config file written: /home/user/.config/pymap/pymap-admin.conf
```

### `login` Command

Sends login credentials and gets a bearer token. See
Expand Down Expand Up @@ -120,7 +131,7 @@ commands. Use `--token-file` or `$PYMAP_ADMIN_TOKEN_FILE` to specify a
location, otherwise it is saved to `~/.pymap-admin.token`.

If `-s` is not given, the `bearer_token` value from the output can provided to
future `pymap-admin` commands with `--token` or `$PYMAP_ADMON_TOKEN`.
future `pymap-admin` commands with `$PYMAP_ADMON_TOKEN`.

### Admin Role

Expand Down
23 changes: 9 additions & 14 deletions pymapadmin/commands/base.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@

from __future__ import annotations

import os
import sys
import traceback
from abc import abstractmethod, ABCMeta
from argparse import ArgumentParser, Namespace
from pathlib import Path
from typing import Generic, Any, Optional, Mapping, TextIO
from typing_extensions import Final

from grpclib.client import Channel

from .. import __version__ as client_version
from ..local import get_token_file
from ..local import token_file
from ..typing import StubT, RequestT, ResponseT, MethodProtocol
from ..grpc.admin_pb2 import SUCCESS

Expand Down Expand Up @@ -82,21 +82,16 @@ def client(self) -> StubT:
"""
...

def _find_token(self, *paths: Path) -> Optional[str]:
for path in paths:
if path.exists():
return path.read_text().strip()
return None

def _get_metadata(self) -> Mapping[str, str]:
metadata = {'client-version': client_version}
if self.args.token is not None:
metadata['auth-token'] = self.args.token
if 'PYMAP_ADMIN_TOKEN' in os.environ:
metadata['auth-token'] = os.environ['PYMAP_ADMIN_TOKEN']
else:
user_token_file = get_token_file(self.args.token_file, user=True)
token_file = get_token_file(None)
token = self._find_token(user_token_file, token_file)
if token is not None:
token: Optional[str] = None
path = token_file.find()
if path is not None:
token = path.read_text().strip()
if token:
metadata['auth-token'] = token
return metadata

Expand Down
36 changes: 30 additions & 6 deletions pymapadmin/commands/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
from contextlib import closing
from typing import Any, Optional, TextIO

from .base import ClientCommand
from ..local import get_token_file
from .base import Command, ClientCommand
from ..config import Config
from ..local import config_file, token_file
from ..typing import RequestT, ResponseT, MethodProtocol
from ..grpc.admin_grpc import SystemStub
from ..grpc.admin_pb2 import LoginRequest, LoginResponse, \
PingRequest, PingResponse

__all__ = ['LoginCommand', 'PingCommand']
__all__ = ['SaveArgsCommand', 'LoginCommand', 'PingCommand']


class SystemCommand(ClientCommand[SystemStub, RequestT, ResponseT]):
Expand All @@ -23,6 +24,29 @@ def client(self) -> SystemStub:
return SystemStub(self.channel)


class SaveArgsCommand(Command):
"""Save the connection settings given as command-line arguments (e.g.
--host, --port, etc) to a config file.
"""

@classmethod
def add_subparser(cls, name: str, subparsers: Any) \
-> ArgumentParser: # pragma: no cover
subparser = subparsers.add_parser(
name, description=cls.__doc__,
help='save connection arguments to config file')
return subparser

async def __call__(self, outfile: TextIO) -> int:
path = config_file.get_home(mkdir=True)
parser = Config.build(self.args)
with open(path, 'w') as cfg:
parser.write(cfg)
print('Config file written:', path, file=outfile)
return 0


class LoginCommand(SystemCommand[LoginRequest, LoginResponse]):
"""Login as a user for future requests."""

Expand Down Expand Up @@ -71,9 +95,9 @@ def handle_success(self, response: LoginResponse, outfile: TextIO) -> None:
super().handle_success(response, outfile)
token = response.bearer_token
if token and self.args.save:
token_file = get_token_file(self.args.token_file, user=True)
token_file.write_text(token)
token_file.chmod(0o600)
path = token_file.get_home(mkdir=True)
path.write_text(token)
path.chmod(0o600)


class PingCommand(SystemCommand[PingRequest, PingResponse]):
Expand Down
123 changes: 123 additions & 0 deletions pymapadmin/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@

from __future__ import annotations

import os
from argparse import Namespace
from configparser import ConfigParser
from typing import Optional

from .local import config_file, token_file, socket_file

__all__ = ['Config']


class Config:
"""Provides values that may be overridden by command-line arguments,
environment variables, or config files (in that order).
Args:
args: The parsed command-line arguments.
"""

def __init__(self, args: Namespace) -> None:
super().__init__()
self._args = args
self._parser = parser = ConfigParser()
parser.read(config_file.get_all())
if not parser.has_section('pymap-admin'):
parser.add_section('pymap-admin')
self._section = section = parser['pymap-admin']
if 'path' in section:
socket_file.add(section['path'])
if 'token_file' in section:
token_file.add(section['token_file'])

@classmethod
def build(cls, args: Namespace) -> ConfigParser:
"""Build and return a new :class:`~configparser.ConfigParser`
pre-loaded with the arguments from *args*.
Args:
args: The command-line arguments to pre-load.
"""
parser = ConfigParser()
parser.add_section('pymap-admin')
section = parser['pymap-admin']
if args.host is not None:
section['host'] = args.host
if args.port is not None:
section['port'] = args.port
if args.path is not None:
section['path'] = args.path
if args.token_file is not None:
section['token_file'] = args.token_file
if args.cert is not None:
section['cert'] = args.cert
if args.key is not None:
section['key'] = args.key
if args.cafile is not None:
section['cafile'] = args.cafile
if args.capath is not None:
section['capath'] = args.capath
if args.no_verify_cert:
section['no_verify_cert'] = str(args.no_verify_cert)
return parser

def _getstr(self, attr: str, envvar: str) -> Optional[str]:
val: Optional[str] = getattr(self._args, attr)
if val is not None:
return val
if envvar in os.environ:
return os.environ[envvar]
return self._section.get(attr, fallback=None)

def _getint(self, attr: str, envvar: str) -> Optional[int]:
val: Optional[int] = getattr(self._args, attr)
if val is not None:
return val
if envvar in os.environ:
return int(os.environ[envvar])
return self._section.getint(attr, fallback=None)

def _getbool(self, attr: str, envvar: str) -> bool:
val: bool = getattr(self._args, attr)
if val:
return True
if envvar in os.environ:
val_str = os.environ[envvar].lower()
return val_str in ('1', 'yes', 'true', 'on')
return self._section.getboolean(attr, fallback=False)

@property
def host(self) -> Optional[str]:
return self._getstr('host', 'PYMAP_ADMIN_HOST')

@property
def port(self) -> Optional[int]:
return self._getint('port', 'PYMAP_ADMIN_PORT')

@property
def token(self) -> Optional[str]:
return self._getstr('token', 'PYMAP_ADMIN_TOKEN')

@property
def cert(self) -> Optional[str]:
return self._getstr('cert', 'PYMAP_ADMIN_CERT')

@property
def key(self) -> Optional[str]:
return self._getstr('key', 'PYMAP_ADMIN_KEY')

@property
def cafile(self) -> Optional[str]:
return self._getstr('cafile', 'PYMAP_ADMIN_CAFILE')

@property
def capath(self) -> Optional[str]:
return self._getstr('capath', 'PYMAP_ADMIN_CAPATH')

@property
def no_verify_cert(self) -> bool:
return self._getbool('no_verify_cert', 'PYMAP_ADMIN_NO_VERIFY_CERT')

0 comments on commit 5d1b19a

Please sign in to comment.