zt 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'xdg'
4
+
5
+ module Zt
6
+ module Constants
7
+ ALL_EXPORTERS = %w[
8
+ HostsFileExporter
9
+ ].freeze
10
+ ALL_IMPORTERS = %w[
11
+ NetworkImporter
12
+ NodeImporter
13
+ ].freeze
14
+ AUTH_TOKEN_LENGTH = 32
15
+ AUTH_TOKEN_REGEX = /^[A-Za-z0-9]+$/.freeze
16
+ CONF_DIR = "#{XDG['CONFIG_HOME']}/zt"
17
+ CONF_SECTIONS = {
18
+ domains: 'domains.yaml',
19
+ networks: 'networks.yaml',
20
+ nodes: 'nodes.yaml',
21
+ zt: 'zt.conf.yaml'
22
+ }.freeze
23
+ # noinspection RubyStringKeysInHashInspection
24
+ INITIAL_CONF = {
25
+ 'nodes.yaml' => {
26
+ 'node_id' => {
27
+ 'hostname' => 'node-hostname',
28
+ 'networks' => %w[network_id_1 network_id_2]
29
+ }
30
+ },
31
+ 'networks.yaml' => {
32
+ 'network_id' => {
33
+ 'name' => 'network_name'
34
+ }
35
+ },
36
+ 'domains.yaml' => {
37
+ 'network_id' => 'domain-name.zt'
38
+ },
39
+ 'zt.conf.yaml' => {
40
+ 'manual_pulls' => false,
41
+ 'token' => 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
42
+ 'top_level_domain' => 'zt'
43
+ }
44
+ }.freeze
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zt/errors/conf_errors'
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zt
4
+ module Errors
5
+ # Config does not exist on disk and cannot be created
6
+ class ZtConfDiskError < StandardError
7
+ def initialize
8
+ super
9
+ end
10
+ end
11
+
12
+ # Config exists on disk but cannot be parsed
13
+ class ZtConfSyntaxError < StandardError
14
+ def initialize
15
+ super
16
+ end
17
+ end
18
+
19
+ # Invalid section specifier for config
20
+ class ZtConfInvalidSectionError < StandardError
21
+ def initialize
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zt/constants'
4
+ require 'zt/exporters/_base_exporter'
5
+ require 'zt/exporters/hosts_file_exporter'
6
+
7
+ module Zt
8
+ module Exporters
9
+ class Exporter
10
+ def initialize(*exporter_names)
11
+ exporter_names = Zt::Constants::ALL_EXPORTERS if exporter_names.empty?
12
+
13
+ exporter_classes = exporter_names.map { |n| Zt::Exporters.const_get(n) }
14
+ @exporters = exporter_classes.map(&:new)
15
+ end
16
+
17
+ # @return [Array[Hash]] an array of hashes from each exporter
18
+ # The hash is in form { data: Object, mangle: lambda }
19
+ # with the :mangle lambda containing any steps to apply the Object
20
+ # like rewriting /etc/hosts or sending the data elsewhere.
21
+ # NOTE there may be value in attributing each hash to its
22
+ # importer, consider that for later as a Hash[Hash].
23
+ def export
24
+ @exporters.map(&:export)
25
+ end
26
+
27
+ def export_one_format(format)
28
+ Zt::Exporters::HostsFileExporter.new.export if format == :hosts
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zt
4
+ module Exporters
5
+ class BaseExporter
6
+ attr_accessor :data
7
+ def initialize
8
+ super
9
+ end
10
+
11
+ def export
12
+ abort('export called on non-functional superclass exporter')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zt/exporters/_base_exporter'
4
+ require 'zt/conf'
5
+
6
+ module Zt
7
+ module Exporters
8
+ class HostsFileExporter < BaseExporter
9
+ def export
10
+ # process the normalised Hash @data and return a String and a Block
11
+ # defining what to do with the String
12
+ conf = Zt::Conf.instance.conf
13
+ output = ''
14
+ conf.nodes.each_key do |node_id|
15
+ node = conf.nodes[node_id]
16
+ node_nets = node[:local][:networks]
17
+ node_nets.each_key do |net_id|
18
+ net_zone = conf.networks[net_id][:local][:dns_zone]
19
+ node_name = node[:remote][:node_name]
20
+ node_addrs = node[:local][:networks][net_id]
21
+ node_addrs.each do |node_addr|
22
+ output += "#{node_addr}\t\t#{node_name}.#{net_zone}\n"
23
+ end
24
+ end
25
+ end
26
+ "# BEGIN zt\n" + output.lines.sort_by do |a|
27
+ a.split[1]
28
+ end.join + "# END zt\n"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zt/constants'
4
+ require 'zt/importers/_base_importer'
5
+ require 'zt/importers/network_importer'
6
+ require 'zt/importers/node_importer'
7
+
8
+ module Zt
9
+ module Importers
10
+ class Importer
11
+ attr_accessor :importers
12
+ def initialize(networks, nodes, *importer_names)
13
+ importer_names = Zt::Constants::ALL_IMPORTERS if importer_names.empty?
14
+ importer_classes = importer_names.map do |n|
15
+ Zt::Importers.const_get(n)
16
+ end
17
+ @importers = importer_classes.map { |c| c.new(networks, nodes) }
18
+ end
19
+
20
+ # @return [Array[Hash]] an array of hashes from each exporter.
21
+ # NOTE there may be value in attributing each hash to its
22
+ # importer, consider that for later as a Hash[Hash].
23
+ def import
24
+ @importers.map(&:import)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zt/conf'
4
+
5
+ module Zt
6
+ module Importers
7
+ class BaseImporter
8
+ attr_accessor :networks
9
+ attr_accessor :nodes
10
+ def initialize(networks, nodes)
11
+ @networks = networks
12
+ @nodes = nodes
13
+ end
14
+
15
+ def import
16
+ abort('import called on non-functional superclass importer')
17
+ end
18
+
19
+ private
20
+
21
+ def dnsify(input)
22
+ zt_conf = Zt::Conf.instance.conf.zt
23
+ tld_key = 'top_level_domains'
24
+ tld = if zt_conf.key?(tld_key) && !zt_conf[tld_key].empty?
25
+ zt_conf[tld_key]
26
+ else
27
+ 'zt'
28
+ end
29
+ clean = input.tr('-', '_')
30
+ clean = clean.gsub(/([\W])/, '-')
31
+ clean = clean.tr('_', '-')
32
+ clean = clean.gsub(/(-+)/, '-')
33
+ clean = "network-#{clean}" unless clean.match?(/^[A-Za-z]/)
34
+ clean = clean.gsub(Regexp.new("-#{tld}$"), '')
35
+ clean.gsub(/(-+)/, '-')
36
+ end
37
+
38
+ def qualify(zone)
39
+ zt_conf = Zt::Conf.instance.conf.zt
40
+ "#{dnsify(zone)}.#{zt_conf['top_level_domain']}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zt/conf'
4
+ require 'zt/importers/_base_importer'
5
+
6
+ module Zt
7
+ module Importers
8
+ class NetworkImporter < BaseImporter
9
+ def import
10
+ output = {}
11
+ # normalise data
12
+ normalised_networks = networks.map do |n|
13
+ {
14
+ network_id: n['id'],
15
+ network_name: n['config']['name'],
16
+ network_description: n['description'],
17
+ network_total_members: n['totalMemberCount'],
18
+ network_authorized_members: n['authorizedMemberCount'],
19
+ network_pending_members:
20
+ (n['totalMemberCount'] - n['authorizedMemberCount'])
21
+ }
22
+ end
23
+ domains_conf = Zt::Conf.instance.conf.domains
24
+ normalised_networks.each do |n|
25
+ zone = if domains_conf.key? n[:network_id]
26
+ if n[:network_id].empty?
27
+ qualify(n[:network_id])
28
+ else
29
+ qualify(domains_conf[n[:network_id]])
30
+ end
31
+ else
32
+ qualify(n[:network_name])
33
+ end
34
+ output[n[:network_id]] = {} unless output.key?(n[:network_id])
35
+ output[n[:network_id]][:remote] = n
36
+ output[n[:network_id]][:local] = {
37
+ dns_zone: zone
38
+ }
39
+ end
40
+ {
41
+ networks: output
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zt/importers/_base_importer'
4
+
5
+ module Zt
6
+ module Importers
7
+ class NodeImporter < BaseImporter
8
+ def import
9
+ output = {}
10
+ hostnames = {}
11
+ memberships = {}
12
+ # normalise data
13
+ nodes.each_key do |network_id|
14
+ net = nodes[network_id]
15
+ normalised_nodes = net.map do |node|
16
+ {
17
+ node_id: node['nodeId'],
18
+ node_name: node['name'],
19
+ node_addr: node['config']['ipAssignments'],
20
+ node_authed: node['config']['authorized']
21
+ }
22
+ end
23
+ normalised_nodes.each do |n|
24
+ hostnames[n[:node_id]] = [] unless hostnames.key?(n[:node_id])
25
+ memberships[n[:node_id]] = [] unless memberships.key?(n[:node_id])
26
+
27
+ hostnames[n[:node_id]].append(n[:node_name])
28
+ memberships[n[:node_id]].append([network_id, n[:node_addr]])
29
+ n.delete(:node_addr)
30
+ output[n[:node_id]] = {} unless output.key?(n[:node_id])
31
+ output[n[:node_id]][:remote] = n
32
+ output[n[:node_id]][:local] = {}
33
+ end
34
+ output.each_key do |k|
35
+ output[k][:remote][:node_name] = hostnames[k].max_by do |i|
36
+ hostnames[k].count(i)
37
+ end
38
+ memberships[k].each do |m|
39
+ output[k][:local][:networks] = {} unless
40
+ output[k][:local].key? :networks
41
+
42
+ output[k][:local][:networks][m[0]] = m[1]
43
+ end
44
+
45
+ end
46
+ end
47
+ {
48
+ nodes: output
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'zt/conf'
5
+
6
+ module Zt
7
+ module RemoteAPI
8
+ class ZeroTierAPI
9
+ include HTTParty
10
+
11
+ base_uri 'https://my.zerotier.com/api'
12
+
13
+ def initialize
14
+ @ztc = Zt::Conf.instance.conf
15
+ # noinspection RubyStringKeysInHashInspection
16
+ @req_opts = {
17
+ headers: {
18
+ 'Authorization' => "Bearer #{@ztc.zt['token']}"
19
+ }
20
+ }
21
+ end
22
+
23
+ def networks
24
+ get_parsed('/network')
25
+ end
26
+
27
+ def network_members(network_id)
28
+ get_parsed("/network/#{network_id}/member")
29
+ end
30
+
31
+ private
32
+
33
+ def get_parsed(path)
34
+ self.class.get(path, @req_opts).parsed_response
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zt
4
+ VERSION = '0.1.1'
5
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'zt/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'zt'
9
+ spec.version = Zt::VERSION
10
+ spec.authors = ['Dave Williams']
11
+ spec.email = ['dave@dave.io']
12
+
13
+ spec.summary = 'ZeroTier administration toolbox'
14
+ spec.description =
15
+ 'Utilities and glue to make working with ZeroTier networks ' \
16
+ 'a bit more friendly'
17
+ spec.homepage = 'https://github.com/daveio/zt'
18
+ spec.license = 'MIT'
19
+
20
+
21
+ if spec.respond_to?(:metadata)
22
+ # spec.metadata['allowed_push_host'] =
23
+ # 'http://localhost'
24
+ spec.metadata['homepage_uri'] = spec.homepage
25
+ spec.metadata['source_code_uri'] = 'https://github.com/daveio/zt'
26
+ spec.metadata['changelog_uri'] =
27
+ 'https://raw.githubusercontent.com/daveio/zt/master/CHANGELOG.md'
28
+ else
29
+ raise 'RubyGems 2.0 or newer is required to protect ' \
30
+ 'against public gem pushes.'
31
+ end
32
+
33
+ # Specify which files should be added to the gem when it is released.
34
+ # The `git ls-files -z` loads the files in the RubyGem that have been
35
+ # added into git.
36
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
37
+ `git ls-files -z`.split("\x0").reject do |f|
38
+ f.match(%r{^(test|spec|features)/})
39
+ end
40
+ end
41
+ spec.bindir = 'exe'
42
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
43
+ spec.require_paths = ['lib']
44
+
45
+ spec.add_dependency 'httparty', '~> 0.0', '>= 0.10.0'
46
+ spec.add_dependency 'thor', '~> 0.0'
47
+ spec.add_dependency 'xdg', '~> 2.0'
48
+
49
+ spec.add_development_dependency 'bundler', '~> 2.0'
50
+ spec.add_development_dependency 'dotenv', '~> 2.0'
51
+ spec.add_development_dependency 'pry', '~> 0.0'
52
+ spec.add_development_dependency 'rake', '~> 12.0'
53
+ spec.add_development_dependency 'rspec', '~> 3.0'
54
+ spec.add_development_dependency 'rubocop', '~> 0.0', '>= 0.49.0'
55
+ spec.add_development_dependency 'rubocop-performance', '~> 1.0'
56
+ spec.add_development_dependency 'ruby-debug-ide', '~> 0.0'
57
+ end