ucmt 0.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2e407946f8dd4efc496b4232ae162ba9dc1bedb0bb9e530a0ba50d216d33f019
4
+ data.tar.gz: fb13b4a84d0452f2458bda86a950f4cd674e76e83662eaa4fdeed3aefbd912d9
5
+ SHA512:
6
+ metadata.gz: db9af1c0b72d1154028c1de3789aeae0f3b940754ca5c2285cf34b115fa3f511c2c9472ca75ccd5a8daf3be466d0762d286f6ba387fade942f7a45258c35e653
7
+ data.tar.gz: 0c7dec652069964770fd42b06e3b65a240642a905d308370da4809efa68dd5e7907daa4135607cfb96aa10fc89e5313a78e47b14065a6e6925c23896a671255a
data/bin/ucmt ADDED
@@ -0,0 +1,36 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require 'optimist'
4
+ require "cheetah"
5
+
6
+ bin_dirs = ENV["PATH"].split(":")
7
+ commands = bin_dirs.each_with_object([]) do |dir, res|
8
+ Dir["#{dir}/ucmt-*"].each do |cmd|
9
+ res << File.basename(cmd).delete_prefix("ucmt-")
10
+ end
11
+ end
12
+
13
+ commands.uniq!
14
+ commands.sort!
15
+
16
+ opts = Optimist.options do
17
+ opt :list_commands, "List all available commands"
18
+ stop_on commands
19
+ end
20
+
21
+ if opts[:list_commands]
22
+ puts "Avaible commands:"
23
+ commands.each do |cmd|
24
+ help = Cheetah.run("ucmt-#{cmd}", "--help", stdout: :capture).lines.first.chomp
25
+ puts "#{cmd}\t#{help}"
26
+ end
27
+
28
+ exit 0
29
+ end
30
+
31
+ Optimist::educate if ARGV.empty?
32
+ Optimist::die "Unknown command '#{ARGV.first}'" unless commands.include?(ARGV.first)
33
+
34
+ cmd = ARGV.shift
35
+ ARGV.unshift("ucmt-#{cmd}")
36
+ exec(*ARGV)
data/bin/ucmt-ansible ADDED
@@ -0,0 +1,38 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require "yaml"
4
+
5
+ require "ucmt/ansible"
6
+ require 'optimist'
7
+ opts = Optimist::options do
8
+ banner "Ansible backend to write salt configuration from UCMT configuration, run it or dry run it."
9
+ opt :config, "Create salt configuration from ucmt configuration. Can be passed also as STDIN.", type: String
10
+ opt :ansible_directory, "Where to write respective read states.yml", type: String, default: "~/"
11
+ opt :dry_run, "Just prints state and what actions will be applied"
12
+ opt :apply, "Apply configuration to system"
13
+ end
14
+ Optimist::die :apply, "Cannot have apply and dry_run together" if opts[:dry_run] && opts[:apply]
15
+
16
+ if opts[:config]
17
+ data = YAML.load_file(opts[:config])
18
+ else
19
+ stdin = STDIN.tty? ? nil : STDIN.read
20
+ data = YAML.load(stdin) if stdin && !stdin.empty?
21
+ end
22
+
23
+ ansible = UCMT::Ansible.new(File.expand_path(opts[:ansible_directory]))
24
+ nocmd = true
25
+ if data
26
+ ansible.write(data)
27
+ nocmd = false
28
+ end
29
+ if opts[:dry_run]
30
+ ansible.dry_run
31
+ nocmd = false
32
+ end
33
+ if opts[:apply]
34
+ ansible.apply
35
+ nocmd = false
36
+ end
37
+
38
+ Optimist::die "At least one of UCMT configuration/apply/dry-run has to be specified." if nocmd
@@ -0,0 +1,22 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require "yaml"
4
+ require 'optimist'
5
+
6
+ require "ucmt/discovery/local_users"
7
+
8
+ def write(io, data)
9
+ io.puts(data.to_yaml)
10
+ end
11
+
12
+ opts = Optimist::options do
13
+ banner "Creates UCMT configuration from system configuration.\nCreated configuration contains more information when run as root user."
14
+ opt :path, "Path to UCMT configuration", type: String
15
+ end
16
+
17
+ users = UCMT::Discovery::LocalUsers.new.read_data
18
+ if opts[:path]
19
+ File.open(opts[:path], "w") { |f| write(f, users) }
20
+ else
21
+ write(STDOUT, users)
22
+ end
data/bin/ucmt-salt ADDED
@@ -0,0 +1,39 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require "yaml"
4
+
5
+ require "ucmt/salt"
6
+ require 'optimist'
7
+
8
+ opts = Optimist::options do
9
+ banner "Salt backend to write salt configuration from UCMT configuration, run it or dry run it."
10
+ opt :config, "Create salt configuration from ucmt configuration. Can be passed also as STDIN.", type: String
11
+ opt :salt_directory, "Where to write respective read salt configuration", type: String, default: "/srv/salt"
12
+ opt :dry_run, "Just prints state and what actions will be applied"
13
+ opt :apply, "Apply configuration to system"
14
+ end
15
+ Optimist::die :apply, "Cannot have apply and dry_run together" if opts[:dry_run] && opts[:apply]
16
+
17
+ if opts[:config]
18
+ data = YAML.load_file(opts[:config])
19
+ else
20
+ stdin = STDIN.tty? ? nil : STDIN.read
21
+ data = YAML.load(stdin) if stdin && !stdin.empty?
22
+ end
23
+
24
+ salt = UCMT::Salt.new(opts[:salt_directory])
25
+ nocmd = true
26
+ if data
27
+ salt.write(data)
28
+ nocmd = false
29
+ end
30
+ if opts[:dry_run]
31
+ salt.dry_run
32
+ nocmd = false
33
+ end
34
+ if opts[:apply]
35
+ salt.apply
36
+ nocmd = false
37
+ end
38
+
39
+ Optimist::die "At least one of UCMT configuration/apply/dry-run has to be specified." if nocmd
data/bin/ucmt-users ADDED
@@ -0,0 +1,109 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require "yaml"
4
+ require 'optimist'
5
+
6
+ require 'ucmt/users'
7
+
8
+ def write(io, data)
9
+ io.puts(data.to_yaml)
10
+ end
11
+
12
+ subcommands = [ "add", "edit", "remove", "ignore", "list", "show"]
13
+
14
+ user_opts = Optimist::options do
15
+ banner("CLI for user managent in UCMT configuration.\n" \
16
+ "Configuration can be passed on stdin and then modified one to stdout or edit in place when using config option.\n" \
17
+ "Usage:\n\n" \
18
+ " ucmt-users <users options> <action> <action options>\n\n" \
19
+ "Available actions: add, edit, remove, ignore, list, show\n" \
20
+ "Use ucmt-users <action> --help for more details for specific actions.\n" \
21
+ "Users options:\n")
22
+ opt :config, "Configuration to edit in place.", type: String
23
+ stop_on subcommands
24
+ end
25
+
26
+ if user_opts[:config]
27
+ data = File.exist?(user_opts[:config]) && YAML.load_file(user_opts[:config])
28
+ else
29
+ stdin = STDIN.tty? ? nil : STDIN.read
30
+ data = YAML.load(stdin) if stdin && !stdin.empty?
31
+ end
32
+ data ||= {}
33
+
34
+ users = UCMT::Users.new(data)
35
+
36
+ write_result = false
37
+
38
+ loop do
39
+ command = ARGV.shift
40
+ case command
41
+ when "add", "edit"
42
+ write_result = true
43
+ cmd_opts = Optimist::options do
44
+ banner "Adds/Edits user"
45
+ opt :name, "Name of user. Mandatory argument.", type: String
46
+ opt :fullname, "Full name of user.", type: String
47
+ opt :no_fullname, "Do not specify full name of user."
48
+ opt :uid, "User ID number.", type: Integer
49
+ opt :no_uid, "Do not specify User ID."
50
+ opt :primary_group, "User primary group specified by name.", type: String
51
+ opt :no_primary_group, "Do not specify primary group."
52
+ opt :shell, "User shell.", type: String
53
+ opt :no_shell, "Do not specify user shell."
54
+ opt :home, "User home directory.", type: String
55
+ opt :no_home, "Do not specify user home directory."
56
+ # TODO: password support
57
+ # opt :password, "Set user password. Both already encrypted and plain password is accepted. Always stored as encrypted.", type: String
58
+ # opt :no_password, "Do not specify user password."
59
+ # opt :forbid_logging, "Do not allow user to login"
60
+ stop_on subcommands
61
+ end
62
+ Optimist::die :name, "Name is mandatory" unless cmd_opts[:name]
63
+ users.edit(cmd_opts)
64
+ when "remove"
65
+ write_result = true
66
+ cmd_opts = Optimist::options do
67
+ banner "Marks user to be removed."
68
+ opt :name, "Name of user. Mandatory argument.", type: String
69
+ stop_on subcommands
70
+ end
71
+ Optimist::die :name, "Name is mandatory" unless cmd_opts[:name]
72
+ users.remove(cmd_opts[:name])
73
+ when "ignore"
74
+ write_result = true
75
+ cmd_opts = Optimist::options do
76
+ banner "Mark user to not be modified."
77
+ opt :name, "Name of user. Mandatory argument.", type: String
78
+ stop_on subcommands
79
+ end
80
+ Optimist::die :name, "Name is mandatory" unless cmd_opts[:name]
81
+ users.ignore(cmd_opts[:name])
82
+ when "list"
83
+ cmd_opts = Optimist::options do
84
+ banner "List all user to modify."
85
+ stop_on subcommands
86
+ end
87
+ users.list
88
+ when "show"
89
+ cmd_opts = Optimist::options do
90
+ banner "Mark user to not be modified."
91
+ opt :name, "Name of user. Mandatory argument.", type: String
92
+ stop_on subcommands
93
+ end
94
+ Optimist::die :name, "Name is mandatory" unless cmd_opts[:name]
95
+ users.show(cmd_opts[:name])
96
+ when nil
97
+ break
98
+ else
99
+ Optimist.die "Invalid action '#{command}'"
100
+ end
101
+ end
102
+
103
+ if write_result
104
+ if user_opts[:config]
105
+ File.open(user_opts[:config], "w") { |f| write(f, data) }
106
+ else
107
+ write(STDOUT, data)
108
+ end
109
+ end
@@ -0,0 +1,76 @@
1
+ require "yaml"
2
+ require "cheetah"
3
+ require "fileutils"
4
+
5
+ module UCMT
6
+ class Ansible
7
+ def initialize(output_dir)
8
+ @output_dir = output_dir
9
+ end
10
+
11
+ def write(data)
12
+ FileUtils.mkdir_p(@output_dir)
13
+
14
+ result = local_users_content(data)
15
+
16
+ content = [{
17
+ "name" => "UCMT defined tasks",
18
+ "hosts" => "localhost",
19
+ "connection" => "local",
20
+ "tasks" => result
21
+ }]
22
+
23
+ File.write(File.join(@output_dir, "states.yml"), content.to_yaml)
24
+ end
25
+
26
+ def dry_run
27
+ Cheetah.run("ansible-playbook", File.join(@output_dir, "states.yml"), "--check", "--diff", stdout: STDOUT)
28
+ end
29
+
30
+ def apply
31
+ Cheetah.run("ansible-playbook", File.join(@output_dir, "states.yml"), stdout: STDOUT)
32
+ end
33
+
34
+ private
35
+
36
+ USERS_MAPPING = {
37
+ "fullname" => "comment",
38
+ "name" => "name",
39
+ "uid" => "uid",
40
+ "groups" => "groups",
41
+ "primary_group" => "group",
42
+ "shell" => "shell",
43
+ "home" => "home",
44
+ "password" => "password"
45
+ }
46
+ def local_users_content(data)
47
+ result = []
48
+
49
+ users_data = data["local_users"]
50
+ return [] unless users_data
51
+
52
+ (users_data["add"] || []).each do |user|
53
+ res = { "name" => "User #{user["name"]}" }
54
+ key2 = "ansible.builtin.user"
55
+ res[key2] = {}
56
+ USERS_MAPPING.each_pair do |k, v|
57
+ res[key2][v] = user[k] if user[k]
58
+ end
59
+
60
+ result << res
61
+ end
62
+
63
+ (users_data["remove"] || []).each do |user|
64
+ res = { "name" => "remove " + user["name"] }
65
+ key2 = "ansible.builtin.user"
66
+ res[key2] = { "name" => user["name"],
67
+ "state" => "absent", "remove" => "yes" }
68
+
69
+ result << res
70
+ end
71
+
72
+ result
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,96 @@
1
+ require "json"
2
+ require "cheetah"
3
+
4
+ module UCMT
5
+ module Discovery
6
+ class LocalUsers
7
+ # TODO: remote machine
8
+ def initialize
9
+ end
10
+
11
+ def read_data
12
+ {
13
+ "local_users" => {
14
+ "add" => read
15
+ }
16
+ }
17
+ end
18
+
19
+ private
20
+
21
+ def read_users
22
+ output = Cheetah.run("ansible", "localhost", "-m", "getent", "-a", "database=passwd", stdout: :capture)
23
+
24
+ res = JSON.parse(output.sub(/^.*=>/, ""))
25
+ res["ansible_facts"]["getent_passwd"]
26
+ end
27
+
28
+ def read_groups
29
+ output = Cheetah.run("ansible", "localhost", "-m", "getent", "-a", "database=group", stdout: :capture)
30
+
31
+ res = JSON.parse(output.sub(/^.*=>/, ""))
32
+ res["ansible_facts"]["getent_group"]
33
+ end
34
+
35
+ def read_passwords
36
+ output = Cheetah.run("ansible", "localhost", "-m", "getent", "-a", "database=shadow", stdout: :capture)
37
+
38
+ res = JSON.parse(output.sub(/^.*=>/, ""))
39
+ res["ansible_facts"]["getent_shadow"]
40
+ end
41
+
42
+ USERS_KEYS_MAPPING = {
43
+ "uid" => 1,
44
+ "gid" => 2,
45
+ "fullname" => 3,
46
+ "home" => 4,
47
+ "shell" => 5
48
+ }
49
+ INTEGER_KEYS = ["uid", "gid"]
50
+ SYSTEM_USER_LIMIT = 500
51
+
52
+ GROUPS_KEYS_MAPPING = {
53
+ "gid" => 1,
54
+ "users" => 2
55
+ }
56
+
57
+ def read
58
+ # reading shadow need root permissions
59
+ # TODO: for remote check needs to be different
60
+ if Process.euid == 0
61
+ passwd = read_passwords
62
+ else
63
+ passwd = {}
64
+ end
65
+
66
+ groups = read_groups
67
+
68
+ users = read_users.map do |dk, dv|
69
+ USERS_KEYS_MAPPING.each_with_object({"name" => dk}) { |(k, v), r| r[k] = dv[v] }
70
+ end
71
+ users.each { |u| INTEGER_KEYS.each { |i| u[i] = u[i].to_i } }
72
+ users.select! { |v| v["uid"] == 0 || v["uid"] > SYSTEM_USER_LIMIT } # select only non system users
73
+ users.each do |user|
74
+ user["groups"] = []
75
+
76
+ groups.each do |name, group|
77
+ gid = group[GROUPS_KEYS_MAPPING["gid"]].to_i
78
+ group_users = group[GROUPS_KEYS_MAPPING["users"]].split(",") # see man group
79
+
80
+ if user["gid"] == gid || group_users.include?(user["name"])
81
+ user["groups"] << name
82
+ end
83
+
84
+ if user["gid"] == gid
85
+ user["primary_group"] = name
86
+ user.delete("gid")
87
+ end
88
+ end
89
+ user["password"] = passwd[user["name"]].first if passwd[user["name"]]
90
+ end
91
+
92
+ users
93
+ end
94
+ end
95
+ end
96
+ end
data/lib/ucmt/salt.rb ADDED
@@ -0,0 +1,73 @@
1
+ require "yaml"
2
+ require "cheetah"
3
+ require "fileutils"
4
+
5
+ module UCMT
6
+ class Salt
7
+ def initialize(output_dir)
8
+ @output_dir = output_dir
9
+ end
10
+
11
+ def write(data)
12
+ FileUtils.mkdir_p(@output_dir)
13
+
14
+ states = []
15
+ states << "local_users" if write_local_users(data)
16
+
17
+ write_states(states)
18
+ end
19
+
20
+ def dry_run
21
+ Cheetah.run("salt-call", "--local", "--file-root=#{@output_dir}", "state.apply", "test=true", stdout: STDOUT)
22
+ end
23
+
24
+ def apply
25
+ Cheetah.run("salt-call", "--local", "--file-root=#{@output_dir}", "state.apply", stdout: STDOUT)
26
+ end
27
+
28
+ private
29
+
30
+ def write_states(states)
31
+ content = { "base" => { "*" => states } }
32
+
33
+ File.write(File.join(@output_dir, "top.sls"), content.to_yaml)
34
+ end
35
+
36
+ USERS_MAPPING = {
37
+ "fullname" => "fullname",
38
+ "name" => "name",
39
+ "uid" => "uid",
40
+ "groups" => "groups",
41
+ "primary_group" => "gid",
42
+ "shell" => "shell",
43
+ "home" => "home",
44
+ "password" => "password"
45
+ }
46
+ def write_local_users(data)
47
+ result = {}
48
+
49
+ users_data = data["local_users"]
50
+ return false unless users_data
51
+
52
+ (users_data["add"] || []).each do |user|
53
+ key = user["name"]
54
+ key2 = "user.present"
55
+ result[key] = { key2 => [] }
56
+ target = result[key][key2]
57
+ USERS_MAPPING.each_pair do |k, v|
58
+ target << { v => user[k] } if user[k]
59
+ end
60
+ end
61
+
62
+ (users_data["remove"] || []).each do |user|
63
+ key = "remove " + user["name"]
64
+ key2 = "user.absent"
65
+ result[key] = { key2 => [{"name" => user["name"]}] }
66
+ end
67
+
68
+ File.write(File.join(@output_dir, "local_users.sls"), result.to_yaml)
69
+
70
+ return true
71
+ end
72
+ end
73
+ end
data/lib/ucmt/users.rb ADDED
@@ -0,0 +1,89 @@
1
+ require "yaml"
2
+ require "cheetah"
3
+ require "fileutils"
4
+
5
+ module UCMT
6
+ class Users
7
+ attr_reader :data
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def ignore(name)
14
+ (users["add"] || []).delete_if { |u| u["name"] == name }
15
+ (users["remove"] || []).delete_if { |u| u["name"] == name }
16
+ end
17
+
18
+ def remove(name)
19
+ (users["add"] || []).delete_if { |u| u["name"] == name }
20
+ users["remove"] ||= []
21
+ users["remove"] << { "name" => name }
22
+ end
23
+
24
+ def edit(options)
25
+ name = options[:name]
26
+
27
+ # ensure that user is not removed
28
+ (users["remove"] || []).delete_if { |u| u["name"] == name }
29
+ users["add"] ||= []
30
+ user = users["add"].find { |u| u["name"] == name }
31
+ if !user
32
+ user = { "name" => name }
33
+ users["add"] << user
34
+ end
35
+
36
+ handle_option(:fullname, user, options)
37
+ handle_option(:uid, user, options)
38
+ handle_option(:primary_group, user, options)
39
+ handle_option(:shell, user, options)
40
+ handle_option(:home, user, options)
41
+
42
+ # TODO: PASSWORD
43
+ # TODO: groups
44
+ end
45
+
46
+ def list
47
+ puts "Users to add/edit:"
48
+ to_add = (users["add"] || []).map {|u| u["name"]}
49
+ puts to_add.to_yaml unless to_add.empty?
50
+ puts "\nUsers to remove:"
51
+ to_remove = (users["remove"] || []).map {|u| u["name"]}
52
+ puts to_remove.to_yaml unless to_remove.empty?
53
+ puts
54
+ end
55
+
56
+ def show(name)
57
+ remove = (users["remove"] || []).find { |u| u["name"] == name }
58
+ if remove
59
+ puts "User #{name} will be removed."
60
+ return
61
+ end
62
+ add = (users["add"] || []).find { |u| u["name"] == name }
63
+ if !add
64
+ puts "User #{name} won't be modified."
65
+ return
66
+ end
67
+
68
+ puts "User #{name} will have following settings:"
69
+ puts add.to_yaml
70
+ end
71
+
72
+ private
73
+
74
+ def handle_option(key, user, options)
75
+ if options[:"no_#{key}"]
76
+ user.delete(key.to_s)
77
+ elsif options[key]
78
+ user[key.to_s] = options[key]
79
+ end
80
+ end
81
+
82
+ def users
83
+ return @users if @users
84
+
85
+ @data["local_users"] ||= {}
86
+ @users = @data["local_users"]
87
+ end
88
+ end
89
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ucmt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Josef Reidinger
8
+ autorequire:
9
+ bindir: bin/
10
+ cert_chain: []
11
+ date: 2021-03-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: optimist
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: cheetah
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.2
41
+ description: A set of tools to generate configuration for various configuration management
42
+ tools like salt or ansible.
43
+ email: jreidinger@suse.com
44
+ executables:
45
+ - ucmt-discovery
46
+ - ucmt-ansible
47
+ - ucmt-salt
48
+ - ucmt
49
+ - ucmt-users
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - bin/ucmt
54
+ - bin/ucmt-ansible
55
+ - bin/ucmt-discovery
56
+ - bin/ucmt-salt
57
+ - bin/ucmt-users
58
+ - lib/ucmt/ansible.rb
59
+ - lib/ucmt/discovery/local_users.rb
60
+ - lib/ucmt/salt.rb
61
+ - lib/ucmt/users.rb
62
+ homepage: https://github.com/jreidinger/ucmt
63
+ licenses:
64
+ - GPL-2.0
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.7.6.2
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Universal configuration management tool
86
+ test_files: []