geofront.remote — Remote sets

Every RemoteSet is represented as a mapping (which is immutable, or mutable) of alias str to Remote object e.g.:

{
    'web-1': Remote('ubuntu', '192.168.0.5'),
    'web-2': Remote('ubuntu', '192.168.0.6'),
    'web-3': Remote('ubuntu', '192.168.0.7'),
    'worker-1': Remote('ubuntu', '192.168.0.25'),
    'worker-2': Remote('ubuntu', '192.168.0.26'),
    'db-1': Remote('ubuntu', '192.168.0.50'),
    'db-2': Remote('ubuntu', '192.168.0.51'),
}

However, in the age of the cloud, you don’t have to manage the remote set since the most of cloud providers offer their API to list provisioned remote nodes.

Geofront provides builtin CloudRemoteSet, a subtype of RemoteSet (which is alias of Mapping[str, Remote]), that proxies to the list dynamically made by cloud providers.

Changed in version 0.2.0: CloudRemoteSet is moved from this module to geofront.backends.cloud. See CloudRemoteSet.

class geofront.remote.AuthorizedKeyList(sftp_client: paramiko.sftp_client.SFTPClient) → None

List-like abstraction for remote authorized_keys.

Note that the contents are all lazily evaluated, so in order to pretend heavy duplicate communications over SFTP use list() to eagerly evaluate e.g.:

lazy_list = AuthorizedKeyList(sftp_client)
eager_list = list(lazy_list)
# ... some modifications on eager_list ...
lazy_list[:] = eager_list
Parameters:sftp_client (paramiko.sftp_client.SFTPClient) – the remote sftp connection to access authorized_keys
FILE_PATH = '.ssh/authorized_keys'

(str) The path of authorized_keys file.

class geofront.remote.DefaultPermissionPolicy

All remotes are listed and allowed for everyone in the team.

New in version 0.2.0.

class geofront.remote.GroupMetadataPermissionPolicy(metadata_key: str, separator: str = None) → None

Allow/disallow remotes according their metadata. It assumes every remote has a metadata key that stores a set of groups to allow. For example, suppose there’s the following remote set:

{
    'web-1': Remote('ubuntu', '192.168.0.5', metadata={'role': 'web'}),
    'web-2': Remote('ubuntu', '192.168.0.6', metadata={'role': 'web'}),
    'web-3': Remote('ubuntu', '192.168.0.7', metadata={'role': 'web'}),
    'worker-1': Remote('ubuntu', '192.168.0.25',
                       metadata={'role': 'worker'}),
    'worker-2': Remote('ubuntu', '192.168.0.26',
                       metadata={'role': 'worker'}),
    'db-1': Remote('ubuntu', '192.168.0.50', metadata={'role': 'db'}),
    'db-2': Remote('ubuntu', '192.168.0.51', metadata={'role': 'db'})
}

and there are groups identified as 'web', 'worker', and 'db'. So the following policy would allow only members who belong to the corresponding groups:

GroupMetadataPermissionPolicy(‘role’)
Parameters:
  • metadata_key (str) – the key to find corresponding groups in metadata of each remote
  • separator (str) – the character separates multiple group identifiers in the metadata value. for example, if the groups are stored as like 'sysadmin,owners' then it should be ','. it splits group identifiers by all whitespace characters by default

New in version 0.2.0.

class geofront.remote.PermissionPolicy

Permission policy determines which remotes are visible by a team member, and which remotes are allowed to SSH. So each remote can have one of three states for each team member:

Listed and allowed
A member can SSH to the remote.
Listed but disallowed
A member can be aware of the remote, but cannot SSH to it.
Unlisted and disallowed
A member can’t be aware of the remote, and can’t SSH to it either.
Unlisted but allowed
It is possible in theory, but mostly meaningless in practice.

The implementation of this interface has to implement two methods. One is filter() which determines whether remotes are listed or unlisted. Other one is permit() which determines whether remotes are allowed or disallowed to SSH.

New in version 0.2.0.

filter(remotes: typing.Mapping[str, geofront.remote.Remote], identity: geofront.identity.Identity, groups: typing.AbstractSet[collections.abc.Hashable]) → typing.Mapping[str, geofront.remote.Remote]

Determine which ones in the given remotes are visible to the identity (which belongs to groups). The resulted mapping of filtered remotes has to be a subset of the input remotes.

Parameters:
  • remotes (RemoteSet) – the remotes set to filter. keys are alias strings and values are Remote objects
  • identity (Identity) – the identity that the filtered remotes would be visible to
  • groups (GroupSet) – the groups that the given identity belongs to. every element is a group identifier and Hashable
Returns:

the filtered result remote set

Return type:

RemoteSet

permit(remote: geofront.remote.Remote, identity: geofront.identity.Identity, groups: typing.AbstractSet[collections.abc.Hashable]) → bool

Determine whether to allow the given identity (which belongs to groups) to SSH the given remote.

Parameters:
  • remote (Remote) – the remote to determine
  • identity (Identity) – the identity to determine
  • groups (GroupSet) – the groups that the given identity belongs to. every element is a group identifier and Hashable
class geofront.remote.Remote(user: str, host: str, port: int = 22, metadata: typing.Mapping[str, object] = {}) → None

Remote node to SSH.

Parameters:
  • user (str) – the username to ssh
  • host (str) – the host to access
  • port (int) – the port number to ssh. the default is 22 which is the default ssh port
  • metadata (Mapping[str, object]) – optional metadata mapping. keys and values have to be all strings. empty by default

New in version 0.2.0: Added optional metadata parameter.

host = None

(str) The hostname to access.

metadata = None

(Mapping[str, object]) The additional metadata. Note that it won’t affect to hash() of the object, nor ==/= comparison of the object.

New in version 0.2.0.

port = None

(int) The port number to SSH.

user = None

(str) The username to SSH.

geofront.remote.RemoteSet

The abstract type for remote sets. Keys are strings and values are Remote objects.

Alias of AbstractSet[str, Remote].

New in version 0.4.0.

alias of Mapping

class geofront.remote.RemoteSetFilter(filter: typing.Callable[[str, geofront.remote.Remote], bool], remote_set: typing.Mapping[str, geofront.remote.Remote]) → None

It takes a filter function and a remote_set, and then return a filtered set of remotes.

>>> remotes = {
...     'web-1': Remote('ubuntu', '192.168.0.5'),
...     'web-2': Remote('ubuntu', '192.168.0.6'),
...     'web-3': Remote('ubuntu', '192.168.0.7'),
...     'worker-1': Remote('ubuntu', '192.168.0.25'),
...     'worker-2': Remote('ubuntu', '192.168.0.26'),
...     'db-1': Remote('ubuntu', '192.168.0.50'),
...     'db-2': Remote('ubuntu', '192.168.0.51'),
... }
>>> filtered = RemoteSetFilter(
...     lambda a, r: a == 'web' or r.host.endswith('5'),
...     remotes
... )
>>> dict(filtered)
{
    'web-1': Remote('ubuntu', '192.168.0.5'),
    'web-2': Remote('ubuntu', '192.168.0.6'),
    'web-3': Remote('ubuntu', '192.168.0.7'),
    'worker-1': Remote('ubuntu', '192.168.0.25')
}

The key difference of this and conditional dict comprehension is evaluation time. (TL;DR: the contents of RemoteSetFilter is evaluated everytime its filtered result is needed.)

If remote_set is an ordinary dict object, RemoteSetFilter is not needed. But if remote_set is, for example, CloudRemoteSet, the filtered result of dict comprehension on it is fixed at Geofront’s configuration loading time. That means geofront-cli remotes doesn’t change even if the list of remotes in the cloud is changed.

On the other hand, the filtered result of RemoteSetFilter is never fixed, because the filter on remote_set is always evaluated again when its __iter__()/__getitem__()/etc are called.

>>> remotes['web-4'] = Remote('ubuntu', '192.168.0.8')
>>> del remotes['worker-1']
>>> dict(filtered)
{
    'web-1': Remote('ubuntu', '192.168.0.5'),
    'web-2': Remote('ubuntu', '192.168.0.6'),
    'web-3': Remote('ubuntu', '192.168.0.7'),
    'web-4': Remote('ubuntu', '192.168.0.25'),  # this replaced worker-1!
}
Parameters:
  • filter (Callable[[str, Remote], bool]) – a filter function which takes key (alias name) and Remote, and False if exclude it, or True if include it
  • remote_set (RemoteSet) – a set of remotes. it has to be a mapping of alias name to Remote

New in version 0.3.1.

class geofront.remote.RemoteSetUnion(*remote_sets) → None

It takes two or more remote sets, and then return a union set of them. Note that the order of arguments affect overriding of aliases (keys). If there are any duplicated aliases (keys), the latter alias (key) is prior to the former.

>>> a = {
...     'web-1': Remote('ubuntu', '192.168.0.5'),
...     'web-2': Remote('ubuntu', '192.168.0.6'),
...     'web-3': Remote('ubuntu', '192.168.0.7'),
...     'worker-1': Remote('ubuntu', '192.168.0.8'),
... }
>>> b = {
...     'worker-1': Remote('ubuntu', '192.168.0.25'),
...     'worker-2': Remote('ubuntu', '192.168.0.26'),
...     'db-1': Remote('ubuntu', '192.168.0.27'),
...     'db-2': Remote('ubuntu', '192.168.0.28'),
...     'db-3': Remote('ubuntu', '192.168.0.29'),
... }
>>> c = {
...     'web-1': Remote('ubuntu', '192.168.0.49'),
...     'db-1': Remote('ubuntu', '192.168.0.50'),
...     'db-2': Remote('ubuntu', '192.168.0.51'),
... }
>>> union = RemoteSetUnion(a, b, c)
>>> dict(union)
{
    'web-1': Remote('ubuntu', '192.168.0.49'),
    'web-2': Remote('ubuntu', '192.168.0.6'),
    'web-3': Remote('ubuntu', '192.168.0.7'),
    'worker-1': Remote('ubuntu', '192.168.0.25'),
    'worker-2': Remote('ubuntu', '192.168.0.26'),
    'db-1': Remote('ubuntu', '192.168.0.50'),
    'db-2': Remote('ubuntu', '192.168.0.51'),
    'db-3': Remote('ubuntu', '192.168.0.29'),
}

Note that RemoteSetUnion is evaluated everytime its contents is needed, like RemoteSetFilter:

>>> del c['web-1']
>>> dict(union)
{
    'web-1': Remote('ubuntu', '192.168.0.5'),  # changed!
    'web-2': Remote('ubuntu', '192.168.0.6'),
    'web-3': Remote('ubuntu', '192.168.0.7'),
    'worker-1': Remote('ubuntu', '192.168.0.25'),
    'worker-2': Remote('ubuntu', '192.168.0.26'),
    'db-1': Remote('ubuntu', '192.168.0.50'),
    'db-2': Remote('ubuntu', '192.168.0.51'),
    'db-3': Remote('ubuntu', '192.168.0.29'),
}
Parameters:*remote_sets (RemoteSet) – two or more remote sets. every remote set has to be a mapping of alias str to Remote

New in version 0.3.2.

geofront.remote.authorize(public_keys: typing.AbstractSet[paramiko.pkey.PKey], master_key: paramiko.pkey.PKey, remote: geofront.remote.Remote, timeout: datetime.timedelta) → datetime.datetime

Make an one-time authorization to the remote, and then revokes it when timeout reaches soon.

Parameters:
Returns:

the expiration time

Return type:

datetime.datetime