Skip to content

Instantly share code, notes, and snippets.

@gamingrobot
Last active September 9, 2022 03:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gamingrobot/d56bfde110a093b1324d91de0a38e6b5 to your computer and use it in GitHub Desktop.
Save gamingrobot/d56bfde110a093b1324d91de0a38e6b5 to your computer and use it in GitHub Desktop.
very basic saltstack module and state for doing things with zerotier-cli and zerotier central api
# -*- coding: utf-8 -*-
'''
Zerotier
'''
# Import python libs
from __future__ import absolute_import, generators, print_function, with_statement, unicode_literals
import logging
import json
# Import 3rd-party libs
# pylint: disable=import-error,no-name-in-module,redefined-builtin
from salt.ext.six.moves.urllib.parse import urljoin as _urljoin
import salt.ext.six.moves.http_client
# pylint: enable=import-error,no-name-in-module
# Import salt libs
import salt.utils
import salt.utils.http
log = logging.getLogger(__name__)
def __virtual__():
'''
Only load the module if zerotier is installed
'''
if salt.utils.which("zerotier-cli"):
return 'zerotier'
return (False, 'The zerotier execution module cannot be loaded: zerotier is not installed.')
def version():
'''
Return version (``zerotier-cli -v``)
CLI Example:
.. code-block:: bash
salt '*' zerotier.version
'''
cmd = 'zerotier-cli -v'
ret = __salt__['cmd.run'](cmd)
return ret
def node_id():
'''
Return node id
CLI Example:
.. code-block:: bash
salt '*' zerotier.node_id
'''
cmd = 'zerotier-cli -j info'
out = __salt__['cmd.run'](cmd)
info = json.loads(salt.utils.to_str(out))
node_address = info["address"]
return node_address
def info(network_id):
'''
Info (``zerotier-cli info``)
CLI Example:
.. code-block:: bash
salt '*' zerotier.info
'''
cmd = 'zerotier-cli -j info'
out = __salt__['cmd.run'](cmd)
ret = json.loads(salt.utils.to_str(out))
return ret
def network_info(network_id):
'''
Network Info
CLI Example:
.. code-block:: bash
salt '*' zerotier.network_info <network_id>
'''
cmd = 'zerotier-cli -j listnetworks'
out = __salt__['cmd.run'](cmd)
decoded = json.loads(salt.utils.to_str(out))
ret = next((n for n in decoded if n['id'] == network_id), None)
return ret
def network_join(network_id):
'''
Join network (``zerotier-cli join <network_id>``)
CLI Example:
.. code-block:: bash
salt '*' zerotier.newtork_join <network_id>
'''
cmd = 'zerotier-cli -j join {0}'.format(network_id)
out = __salt__['cmd.run'](cmd)
ret = json.loads(salt.utils.to_str(out))
return ret
def network_leave(network_id):
'''
Leave network (``zerotier-cli leave <network_id>``)
CLI Example:
.. code-block:: bash
salt '*' zerotier.network_leave <network_id>
'''
cmd = 'zerotier-cli -j leave {0}'.format(network_id)
out = __salt__['cmd.run'](cmd)
ret = json.loads(salt.utils.to_str(out))
return ret
def central_get_member(network_id, api_key=None):
'''
Get information about network memeber
CLI Example:
.. code-block:: bash
salt '*' zerotier.central_get_member <network_id> <api_key>
'''
nodeId = node_id()
base_url = 'https://my.zerotier.com'
url = _urljoin(base_url, '/api/network/{networkId}/member/{nodeId}'.format(networkId=network_id, nodeId=nodeId))
headers = {'Authorization': "Bearer {0}".format(api_key), 'Content-type': 'application/json'}
member_info_raw = salt.utils.http.query(
url,
'GET',
text=True,
status=True,
header_dict=headers,
opts=__opts__
)
if member_info_raw['status'] != 200:
raise Exception(member_info_raw['error'])
#Handle the fact that the result from this GET may be empty
if member_info_raw['text']:
member_info_raw['dict'] = json.loads(salt.utils.to_str(member_info_raw['text']))
return member_info_raw.get('dict', {})
def central_update_member(network_id, api_key=None, **config_args):
'''
Update information about network memeber
CLI Example:
.. code-block:: bash
salt '*' zerotier.central_update_member <network_id> <api_key> <config_args>
config_args:
hidden:bool - Hidden in UI
name:string - Short name describing member
description:string - Long form description
offlineNotifyDelay:number - Notify of offline after this many milliseconds
authorized:bool - True if authorized (only matters on private networks)
capabilities:array - Array of IDs of capabilities assigned to this member
tags:array - Array of tuples of tag ID, tag value
ipAssignments:array - Array of IP assignments published to member
noAutoAssignIps:bool - If true do not auto-assign IPv4 or IPv6 addresses, overriding
'''
nodeId = node_id()
# TODO: check we have network_id and node_id
base_url = 'https://my.zerotier.com'
url = _urljoin(base_url, '/api/network/{networkId}/member/{nodeId}'.format(networkId=network_id, nodeId=nodeId))
headers = {'Authorization': "Bearer {0}".format(api_key), 'Content-type': 'application/json'}
member_info_raw = salt.utils.http.query(
url,
'POST',
decode=True,
status=True,
header_dict=headers,
opts=__opts__
)
if member_info_raw['status'] != 200:
raise Exception(member_info_raw['error'])
member_info = member_info_raw.get('dict', {})
#set properties
root_keys = ['hidden', 'name', 'description', 'offlineNotifyDelay']
config_keys = ['authorized', 'capabilities', 'tags', 'ipAssignments', 'noAutoAssignIps']
for key in root_keys:
if key in config_args and key in member_info:
log.debug("Setting root key {0} to {1}".format(key, config_args[key]))
member_info[key] = config_args[key]
for key in config_keys:
if key in config_args and key in member_info.get('config', {}):
log.debug("Setting config key {0} to {1}".format(key, config_args[key]))
member_info['config'][key] = config_args[key]
update_result = salt.utils.http.query(
url,
'POST',
params={},
data=json.dumps(member_info),
decode=True,
status=True,
header_dict=headers,
opts=__opts__
)
if update_result['status'] != 200:
raise Exception(update_result['error'])
return update_result.get('dict', {})
# -*- coding: utf-8 -*-
'''
Zerotier state
For managing zero tier things
Config Options:
hidden:bool - Hidden in UI
name:string - Short name describing member
description:string - Long form description
offlineNotifyDelay:number - Notify of offline after this many milliseconds
authorized:bool - True if authorized (only matters on private networks)
capabilities:array - Array of IDs of capabilities assigned to this member
tags:array - Array of tuples of tag ID, tag value
ipAssignments:array - Array of IP assignments published to member
noAutoAssignIps:bool - If true do not auto-assign IPv4 or IPv6 addresses, overriding
.. code-block:: yaml
zerotier-membership:
zerotier.joined:
- network_id: abc
zerotier.central_member:
- network_id: abc
- api_key: xyz
- config:
name: testing
description: "hello world"
authorized: True
'''
from __future__ import absolute_import, generators, print_function, with_statement, unicode_literals
import logging
# Import python libs
import os.path
# Import Salt libs
import salt.utils
log = logging.getLogger(__name__)
def joined(name, network_id):
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
if network_id == None:
raise SaltInvocationError('network_id is required')
network_state = __salt__['zerotier.network_info'](network_id)
if network_state != None:
ret['result'] = True
ret['comment'] = 'Already joined network'
return ret
elif __opts__['test']:
ret['comment'] = 'Will join network'
ret['result'] = None
return ret
joined_state = __salt__['zerotier.network_join'](network_id)
if joined_state != None:
ret['changes'] = {
'old': network_state,
'new': joined_state
}
ret['result'] = True
ret['comment'] = 'Successfully joined network'
else:
ret['result'] = False
ret['comment'] = 'Failed to join network'
return ret
def central_member(name, network_id, api_key, config):
ret = {'name': name,
'changes': {},
'result': None,
'comment': ''}
if network_id == None:
raise SaltInvocationError('network_id is required')
if api_key == None:
raise SaltInvocationError('api_key is required')
if __opts__['test']:
ret['comment'] = 'Will update central member'
ret['changes'].update({'member': {'old': '', 'new': config}})
ret['result'] = None
return ret
original_member_info = __salt__['zerotier.central_get_member'](network_id, api_key)
#TODO: check values and see if we even need to update
member_info = __salt__['zerotier.central_update_member'](network_id, api_key, **config)
#Do a diff, prob a better way to do this
diff = {'old': {}, 'new': {}}
root_keys = ['hidden', 'name', 'description', 'offlineNotifyDelay']
config_keys = ['authorized', 'capabilities', 'tags', 'ipAssignments', 'noAutoAssignIps']
for key in root_keys:
if key in original_member_info:
old = original_member_info[key]
if key in member_info:
new = member_info[key]
if old != new:
diff['old'][key] = old
diff['new'][key] = new
for key in config_keys:
if key in original_member_info.get('config', {}):
old = original_member_info['config'][key]
if key in member_info.get('config', {}):
new = member_info['config'][key]
if old != new:
diff['old'][key] = old
diff['new'][key] = new
if not diff['old'] and not diff['new']:
ret['result'] = True
ret['comment'] = 'Central Member is up to date'
else:
#just some visual cleanup if empty dict
if not diff['old']:
diff['old'] = None
if not diff['new']:
diff['new'] = None
ret['changes'] = diff
ret['result'] = True
ret['comment'] = 'Updated central member'
return ret
zerotier-pkgrepo:
pkgrepo.managed:
- humanname: ZeroTier Ubuntu Repo
- name: {{ 'deb https://download.zerotier.com/debian/' + salt['grains.get']('oscodename') + ' ' + salt['grains.get']('oscodename') + ' main'}}
- file: /etc/apt/sources.list.d/zerotier.list
- key_url: https://pgp.mit.edu/pks/lookup?op=get&search=0x1657198823E52A61
- clean_file: True
- require_in:
- pkg: zerotier
zerotier:
pkg.installed:
- name: zerotier-one
service.running:
- name: zerotier-one
- enable: True
- require:
- pkg: zerotier
zerotier-joined:
zerotier.joined:
- network_id: {{ salt['pillar.get']('zerotier:network_id') }}
zerotier-member:
zerotier.central_member:
- network_id: {{ salt['pillar.get']('zerotier:network_id') }}
- api_key: {{ salt['pillar.get']('zerotier:api_key') }}
- config:
name: {{ salt['grains.get']('host') }}
authorized: True
- onchanges:
- zerotier: zerotier-joined
zerotier:
network_id: abcdefg
api_key: xyzabc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment